« All posts

03.30.2022 - 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 type T, which is not assignable to anything in type K:

    type U = Exclude<keyof T, K>
    

    By doing this, we are able to remove all the fields in T that is assignable to K.

  • Finally, since U only contains the fields that are not found in K, 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.

Read more