TypeScript / How some utility types are implemented
TypeScript provides several utility types◹ to help us manipulate types easier, like: Partial<T>
, Required<T>
, Pick<T, Keys>
,…
Internally, these types are implemented in the src/lib/es5.d.ts, at the type level◹. And their implementation details are very interesting.
Prerequisites: Mapped Type
A mapped type is a generic type that iterates through the keys of another type, to create a new type. For example:
type AllBoolean<Type> = {
[Key in keyof Type]: boolean
}
The keyof
keyword returns a union of all the keys inside a type, we then use the index signature syntax to iterate through these keys, map them with the Boolean
type, the end result is a new type containing all the keys of Type
, with the type Boolean
.
Later on, we can use this AllBoolean
type like this:
type Foo = {
name: string;
age: number;
}
type BoolFoo = AllBoolean<Foo>;
// ^? {
// name: boolean,
// age: boolean,
// }
For more details about mapped types, please read the TypeScript Handbook: Mapped Types◹.
Types Impelementations
Now, let’s go over some of the utility-type implementations. Most of them are created using mapped type.
The Partial<T> Type
The Partial<T>
type takes a type T
and makes all of the fields in T
optional. Here’s how Partial<T>
is implemented:
type Partial<T> = {
[P in keyof T]?: T[P];
};
It mapped all the field P
of T
into the type T[P]
(which means, get the type of P
as defined in T
). But also added a ?
modifier for each of the fields.
The ?
modifier indicates that the field is optional.
The Required<T> Type
Opposite from Partial<T>
is Required<T>
, which makes all the fields of type T
become required. It was implemented as:
type Required<T> = {
[P in keyof T]-?: T[P];
};
The way it works is almost identical to how Partial<T>
works, except this time, it removes the optional ?
modifier with the -?
operator.
When the optional modifier is removed from a field, that field becomes required.
The Readonly<T> Type
The Readonly<T>
type makes all the properties in T
become read-only, it does so by annotating each field with the readonly
keyword while mapping:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
The Pick<T, K> Type
Now, this one has much more to talk about. The Pick<T, K>
type takes a type T
and returns a new type that only has the fields defined in the union K
. It is implemented as:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
First, for the type parameters, the list of keys K
is defined as:
K extends keyof T
This means, K
is a union that only contains the fields inside the type T
, so we cannot pass some arbitrary values in here, because it would cause a problem during the mapping phase.
Next, we will iterate through the keys in K
and map to the corresponding field found in the type T
.
The Exclude<T, U> Type
The Exclude<T, U>
type is used to make sure T
will never be any type that is assignable to U
. It does so by checking if the type T
is extendable to U
, returns a never
type, otherwise, return the type T
itself:
type Exclude<T, U> = T extends U ? never : T;
This means, U
can be anything, a primitive type or a union. This type is very interesting because it can be used as a building block for other types, for example: Omit<T, K>
.
The Omit<T, K> Type
As the name implies, the Omit<T, K>
is a reversed version of Pick<T, K>
, it removes all the fields defined in K
from the type T
, with the help of Exclude<T, K>
:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
This type can be interpreted step by step:
First, create a type
U
that is a union of every field in typeT
, which is not assignable to anything in typeK
:type U = Exclude<keyof T, K>
By doing this, we are able to remove all the fields in
T
that is assignable toK
.Finally, since
U
only contains the fields that are not found inK
, we can pick them:type Result = Pick<T, U>
The ReturnType<T> Type
OK, I know you are starting to get a headache already. This is the last one, I promise. The ReturnType<T>
type is used to get the return type of a function type, this one is interesting:
type ReturnType<T extends (...args: any) => any>
= T extends (...args: any) => infer R ? R : any;
Whoa, there’s a lot going on here.
First, the input type parameter of this type means, we take a type T
that has the shape of a function:
T extends (...args: any) => any
In the implementation, we use a conditional type to check if the return type of function type T
can be inferred or not, and call the inferred type here is R
:
T extends (...args: any) => infer R
If it is, we return the inferred type R
, otherwise, it can be anything, hence, any
.