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
Uthat 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
Tthat is assignable toK.Finally, since
Uonly 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.