Editor’s note: This guide to using built-in utility types in TypeScript was last updated by Muhammed Ali on 29 May 2023 to reflect changes to TypeScript and include new sections on why you should use utility types and when to avoid them. To learn more about types, refer to our guide on types vs. interfaces in TypeScript.
TypeScript has one of the most powerful type systems of any programming language out there. This is because it evolved to accommodate all the dynamic things you can do in JavaScript. Including features such as conditional, lookup, and mapped types means that you can write some pretty advanced type functions in TypeScript.
In this article, you will learn about type functions in TypeScript. The article covers several built-in type functions and their real-world use cases to help you understand how to apply them in your TypeScript code.
Jump ahead:
A type function is a function-like construct that operates on types rather than values. It allows you to define and manipulate types in a programmatic manner. Type functions are typically used to transform or derive new types based on existing types, perform conditional type checks, or extract information from types. TypeScript allows you to create a type alias from any existing type. For example:
// Example type Str = string; // Usage let message: Str = 'Hello world';
TypeScript type aliases also support generics. Generics are traditionally used to constrain one type based on another. For example, the type for a value
, setValue
pair, and a generic is used to ensure that setValue
is always invoked with the same type used by value
, as shown below:
// Example type ValueControl<T> = { value: T, setValue: (newValue: T) => void, }; // Usage const example: ValueControl<number> = { value: 0, setValue: (newValue) => example.value = newValue, };
Notice that we can pass in a type for ValueControl
in the above example. In our example, we are passing in the type number
(i.e., ValueControl<number>
).
The truly powerful feature of TypeScript is that the type passed into the generic function can be used in conditions with conditional types and mapping with mapped types. Here is an example of a type that uses conditions to exclude null
and undefined
from a type:
/** * Exclude null and undefined from T */ type NoEmpty<T> = T extends null | undefined ? never : T; // Usage type StrOrNull = string | null; type Str = NoEmpty<StrOrNull>; // string
However, you don’t necessarily need to use these base-level features, as TypeScript also comes with a number of handy built-in utility functions. In fact, our type NoEmpty<T>
already ships as a part of TypeScript (it’s called NonNullable<T>
, and we’ll discuss that later in the article).
Using built-in utility types in TypeScript can provide several benefits. Let’s explore them:
There are certain cases when you might want to avoid using utility types, such as:
Utility types in TypeScript are predefined type functions that provide convenient transformations and operations on existing types. They allow you to create new types based on existing types by modifying or extracting properties, making properties optional or required, creating immutable versions of types, and more.
These utility types are built into TypeScript itself and do not require any additional packages or libraries. They are designed to enhance the expressiveness and flexibility of the type system in TypeScript, enabling you to define more precise types and catch potential errors at compile time.
As of TypeScript v5.0, these are the built-in type functions that you can use in TypeScript without needing any additional packages. Let’s check them out.
Awaited<T>
The Awaited<T>
utility type extracts the resolved type of a promise or an async
function. It unwraps the generic type T
and returns the type that T
resolves to when awaited. Let’s see an example of how the Awaited<T>
utility type can be used:
function fetchData(): Promise<string> { return new Promise((resolve) => { setTimeout(() => { resolve("Data fetched successfully!"); }, 2000); }); } async function process() { const result: Awaited<ReturnType<typeof fetchData>> = await fetchData(); console.log(result); // Output: "Data fetched successfully!" } process();
Using Awaited<ReturnType<typeof fetchData>>
, we infer the resolved type of the promise returned by fetchData
. In this case, ReturnType<typeof fetchData>
gives us the type Promise<string>
and Awaited<Promise<string>>
resolves to string
.
When await fetchData()
is called inside the process
function, it waits for the promise to be fulfilled. The resolved value of the promise is then stored in the result
variable, which is inferred as string
.
Finally, we log the result to the console. By using Awaited<Type>
, we can conveniently work with the resolved value of a promise without explicitly chaining .then()
handlers.
Partial<T>
Partial
marks all the members of an input type T
as being optional. Here is an example with a simple Point
type:
type Point = { x: number, y: number }; // Same as `{ x?: number, y?: number }` type PartialPoint = Partial<Point>;
A common use case is the update
pattern found in many state management libraries, where you only provide a subset of the properties you want to be changed. For example:
class State<T> { constructor(public current: T) { } // Only need to pass in the properties you want changed update(next: Partial<T>) { this.current = { ...this.current, ...next }; } } // Usage const state = new State({ x: 0, y: 0 }); state.update({ y: 123 }); // Partial. No need to provide `x`. console.log(state.current); // Update successful: { x: 0, y: 123 }
Required<T>
Required
does the opposite of Partial<T>
. It makes all the members of an input type T
non-optional. In other words, it makes them required. Here is an example of this transformation:
type PartialPoint = { x?: number, y?: number }; // Same as `{ x: number, y: number }` type Point = Required<PartialPoint>;
A use case of this is when a type has optional members, but portions of your code need all of them to be provided. You can have a config with optional members, but internally, you initialize them so you don’t have to handle null checking all your code, as shown below:
// Optional members for consumers type CircleConfig = { color?: string, radius?: number, } class Circle { // Required: Internally all members will always be present private config: Required<CircleConfig>; constructor(config: CircleConfig) { this.config = { color: config.color ?? 'green', radius: config.radius ?? 0, } } draw() { // No null checking needed :) console.log( 'Drawing a circle.', 'Color:', this.config.color, 'Radius:', this.config.radius ); } }
Readonly<T>
This marks all the properties of the input type T
as readonly
. Here is an example of this transformation:
type Point = { x: number, y: number }; // Same as `{ readonly x: number, readonly y: number }` type ReadonlyPoint = Readonly<Point>;
This is useful for the common pattern of freezing an object to prevent edits. For example:
function makeReadonly<T>(object: T): Readonly<T> { return Object.freeze({ ...object }); } const editablePoint = { x: 0, y: 0 }; editablePoint.x = 2; // Success: allowed const readonlyPoint = makeReadonly(editablePoint); readonlyPoint.x = 3; // Error: Cannot assign to read only property 'x' of object '#<Object>'
Pick<T, Keys>
Picks only the specified Keys
from T
. In the following code, we have a Point3D
with keys 'x' | 'y' | 'z'
, and we can create a Point2D
by only picking the keys 'x' | 'y'
, as shown below:
type Point3D = { x: number, y: number, z: number, }; // Same as `{ x: number, y: number }` type Point2D = Pick<Point3D, 'x' | 'y'>;
This is useful for getting a subset of objects like we’ve seen in the above example by creating Point2D
. A more common use case is to simply get the properties you are interested in. This is demonstrated below, where we get the width
and height
from all the CSSProperties
:
// All the CSSProperties type CSSProperties = { color?: string, backgroundColor?: string, width?: number, height?: number, // ... lots more }; function setSize( element: HTMLElement, // Usage: Just need the size properties size: Pick<CSSProperties, 'width' | 'height'> ) { element.setAttribute('width', (size.width ?? 0) + 'px'); element.setAttribute('height', (size.height ?? 0) + 'px'); }
Record<Keys, Value>
Given a set of member names specified by Keys
, this creates a type where each member is of type Value
. Here is an example that demonstrates this:
// Same as `{x: number, y: number}` type Point = Record<'x' | 'y', number>;
When all the members of a type have the same Value
, using Record
can help your code read better because it’s immediately obvious that all members have the same Value
type. This is slightly visible in the above Point
example.
When there is a large number of members, then Record
is even more useful. Here’s the code without using Record
:
type PageInfo = { id: string, title: string, }; type Pages = { home: PageInfo, services: PageInfo, about: PageInfo, contact: PageInfo, };
Here’s the code using Record
:
type Pages = Record< 'home' | 'services' | 'about' | 'contact', { id: string, title: string } >;
Omit<T, Keys>
This type function omits the keys specified by Keys
from the type T
. Here’s an example:
type Point3D = { x: number, y: number, z: number, }; // Same as `{ x: number, y: number }` type Point2D = Omit<Point3D, 'z'>;
Omitting certain properties from an object before passing it on is a common pattern in JavaScript. The Omit
type function offers a convenient way to annotate such transforms.
It is, for example, conventional to remove PII
(personally identifiable information such as email addresses and names) before logging in. You can annotate this transformation with Omit
, like so:
type Person = { id: string, hasConsent: boolean, name: string, email: string, }; // Utility to remove PII from `Person` function cleanPerson(person: Person): Omit<Person, 'name' | 'email'> { const { name, email, ...clean } = person; return clean; }
Exclude<T, Excluded>
This excludes Excluded
types from T
. Here’s what that looks like:
type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c' type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c' type T2 = Exclude<string | number | (() => void), Function>; // string | number
The most common use case is to exclude certain keys from an object. For example:
type Dimensions3D = 'x' | 'y' | 'z'; type Point3D = Record<Dimensions3D, number>; // Use exclude to create Point2D type Dimensions2D = Exclude<Dimensions3D, 'z'>; type Point2D = Record<Dimensions2D, number>;
You can also use it to exclude other undesirable members (e.g., null
and undefined
) from a union:
type StrOrNullOrUndefined = string | null | undefined; // string type Str = Exclude<StrOrNullOrUndefined, null | undefined>;
NonNullable<T>
This excludes null
and undefined
from the type T
. It has the same effect as Exclude<T, null | undefined>
. Here’s a quick JavaScript premier: nullable is something that can be assigned a nullish value i.e., null
or undefined
. So, non-nullable is something that shouldn’t accept nullish values.
Here is the same example we saw with Exclude
, but this time, let’s use NonNullable
:
type StrOrNullOrUndefined = string | null | undefined; // Same as `string` // Same as `Exclude<StrOrNullOrUndefined, null | undefined>` type Str = NonNullable<StrOrNullOrUndefined>;
Extract<T, Extracted>
This extracts Extracted
types from T
. You can view it as the opposite of Exclude
because instead of specifying which types you want to exclude (Exclude
), you specify which types you want to keep/extract (Extract
). Here’s what that looks like:
type T0 = Extract<'a' | 'b' | 'c', 'a'>; // 'a' type T1 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b' type T2 = Extract<string | number | (() => void), Function>; // () => void
Extract
can be thought of as an intersection of two given types. This is demonstrated below where the common elements 'a' | 'b'
are extracted:
type T3 = Extract<'a' | 'b' | 'c', 'a' | 'b' | 'd'>; // 'a' | 'b'
One use case of Extract
is to find the common base of two types, like so:
type Person = { id: string, name: string, email: string, }; type Animal = { id: string, name: string, species: string, }; /** Use Extract to get the common keys */ type CommonKeys = Extract<keyof Person, keyof Animal>; /** * Map over the keys to find the common structure * Same as `{ id: string, name: string }` **/ type Base = { [K in CommonKeys]: (Animal & Person)[K] };
Parameters<Function>
Given a Function
type, this type returns the types of the function parameters as a tuple. Here is an example that demonstrates this transformation:
function add(a: number, b: number) { return a + b; } // Same as `[a: number, b: number]` type AddParameters = Parameters<typeof add>;
You can combine Parameters
with TypeScript’s index lookup types to get any individual parameter. We can even fetch the type of the first parameter, like so:
function add(a: number, b: number) { return a + b; } // Same as `number` type A = Parameters<typeof add>[0];
A key use case for Parameters
is the ability to capture the type of a function parameter so you can use it in your code to ensure type safety, as shown below:
// A save function in an external library function save(person: { id: string, name: string, email: string }) { console.log('Saving', person); } // Ensure that ryan matches what is expected by `save` const ryan: Parameters<typeof save>[0] = { id: '1337', name: 'Ryan', email: '[email protected]', };
ReturnType<Function>
Given a Function
type, this gets the type returned by the function:
function createUser(name: string) { return { id: Math.random(), name: name }; } // Same as `{ id: number, name: string }` type User = ReturnType<typeof createUser>;
A possible use case is similar to the one we saw with Parameters
. It allows you to get the return type of a function so you can use it to type other variables. This is actually demonstrated in the above example.
You can also use ReturnType
to ensure that the output of one function is the same as the input of another function. This is common in React, where you have a custom Hook that manages the state needed by a React component. Here’s an example:
import React from 'react'; // Custom hook function useUser() { const [name, setName] = React.useState(''); const [email, setEmail] = React.useState(''); return { name, setName, email, setEmail, }; } // Custom component uses the return value of the hook function User(props: ReturnType<typeof useUser>) { return ( <> <div>Name: {props.name}</div> <div>Email: {props.email}</div> </> ); }
ConstructorParameters<ClassConstructor>
This is similar to the Parameters
type we saw above. The only difference is that ConstructorParameters
works on a class constructor, like this:
class Point { private x: number; private y: number; constructor(initial: { x: number, y: number }) { this.x = initial.x; this.y = initial.y; } } // Same as `[initial: { x: number, y: number} ]` type PointParameters = ConstructorParameters<typeof Point>;
And, of course, the main use case for ConstructorParamters
is also similar. In the following example, we use it to ensure that our initial values are something that will be accepted by the Point
class:
class Point { private x: number; private y: number; constructor(initial: { x: number, y: number }) { this.x = initial.x; this.y = initial.y; } } // Ensure that `center` matches what is expected by `Point` constructor const center: ConstructorParameters<typeof Point>[0] = { x: 0, y: 0, };
InstanceType<ClassConstructor>
InstanceType
is similar to ReturnType
we saw above. The only difference is that InstanceType
works on a class constructor. Here’s what that looks like:
class Point { x: number; y: number; constructor(initial: { x: number, y: number }) { this.x = initial.x; this.y = initial.y; } } // Same as `{x: number, y: number}` type PointInstance = InstanceType<typeof Point>;
You wouldn’t normally need to use this for any static class like the Point
class above. This is because you can just use the type annotation Point
, as shown here:
class Point { x: number; y: number; constructor(initial: { x: number, y: number }) { this.x = initial.x; this.y = initial.y; } } // You wouldn't do this const verbose: InstanceType<typeof Point> = new Point({ x: 0, y: 0 }); // Because you can do this const simple: Point = new Point({ x: 0, y: 0 });
However, TypeScript also allows you to create dynamic classes, e.g., the following function DisposibleMixin
returns a class on the fly:
type Class = new (...args: any[]) => any; // creates a class dynamically and returns it function DisposableMixin<Base extends Class>(base: Base) { return class extends base { isDisposed: boolean = false; dispose() { this.isDisposed = true; } }; }
Now, we can use InstanceType
to get the type of instances created by invoking DisposiblePoint
:
type Class = new (...args: any[]) => any; function DisposableMixin<Base extends Class>(base: Base) { return class extends base { isDisposed: boolean = false; dispose() { this.isDisposed = true; } }; } // dynamically created class const DisposiblePoint = DisposableMixin(class { x = 0; y = 0; }); // Same as `{isDisposed, dispose, x, y}` let example: InstanceType<typeof DisposiblePoint>;
As we have seen, there are many built-in utility types that come with TypeScript. Many of these are simple definitions that you can write yourself. For example, if you wanted to exclude null
and undefined
, you could easily write the following yourself:
// Your custom creation type NoEmpty<T> = T extends null | undefined ? never : T; // Usage type StrOrNull = string | null; // string - People need to look at `NoEmpty` to understand what it means type Str = NoEmpty<StrOrNull>;
However, using the built-in version NonNullable
(which does the same thing) can improve readability in your code so people familiar with TypeScript’s standard library don’t need to parse the body T extends null | undefined ? never : T;
in order to understand what is going on.
This is demonstrated below:
// No need for creating something custom // Usage type StrOrNull = string | null; // string - People that know TS know what `NonNullable` does type Str = NonNullable<StrOrNull>;
Ultimately, you should use built-in types like ReadOnly
/ Partial
/ Required
whenever possible instead of creating custom ones. In addition to saving time writing code, it also saves you from having to think about naming your utilities, as these have been named for you by the TypeScript team.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Using built-in utility types in TypeScript"
This is by far the best explanation of Typescripts utility types I have ever read. I also like the sample code which makes it easy to use in your own projects. Thank you very much!
Good read! The content is clear and instructive.