top of page

Mapped Types in TypeScript



Mapped types allow you to create new types from existing ones by mapping over property types. Each property of the existing type is transformed according to a rule that you specify. The transformed properties then make up the new type.


Using mapped types, you can capture the effects of methods such as Object.freeze() in the type system. After an object has been frozen, it's no longer possible to add, change, or remove properties from it. Let's see how we would encode that in the type system without using mapped types:

interface Point {  
    x: number;  
    y: number;
}

interface FrozenPoint {  
    readonly x: number;  
    readonly y: number;
}

function freezePoint(p: Point): FrozenPoint {  
    return Object.freeze(p);
}

const origin = freezePoint({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42;

We're defining a Point interface that contains the two properties x and y. We're also defining another interface, FrozenPoint, which is identical to Point, except that all its properties have been turned into read-only properties using the readonly keyword.


The freezePoint function takes a Point as a parameter, freezes it, and returns the same object to the caller. However, the type of that object has changed to FrozenPoint, so its properties are statically typed as read-only. This is why TypeScript errors when attempting to assign 42 to the x property. At run-time, the assignment would either throw a TypeError (in strict mode) or silently fail (outside of strict mode).


Modeling Object.freeze() with Mapped Types

Let's now see how Object.freeze() is typed within the lib.d.ts file that ships with TypeScript:

/**  
* Prevents the modification of existing property attributes and values, and prevents the addition of new properties.  
* @param o Object on which to lock the attributes.  
*/

freeze<T>(o: T): Readonly<T>;

The method has a return type of Readonly<T> — and that's a mapped type! It's defined as follows:

type Readonly<T> = {  
    readonly [P in keyof T]: T[P];
};

This syntax may look daunting at first, so let's disassemble it piece by piece:

  • We're defining a generic Readonly type with a single type parameter named T.

  • Within the square brackets, we're using the keyof operator. keyof T represents all property names of type T as a union of string literal types.

  • The in keyword within the square brackets signals that we're dealing with a mapped type. [P in keyof T]: T[P] denotes that the type of each property P of type T should be transformed to T[P]. Without the readonly modifier, this would be an identity transformation.

  • The type T[P] is a lookup type. It represents the type of the property P of the type T.

  • Finally, the readonly modifier specifies that each property should be transformed to a read-only property.


Because the type Readonly<T> is generic, Object.freeze() is typed correctly for every type we provide for T. We can now simplify our code from before:

const origin = Object.freeze({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.

origin.x = 42;


An Intuitive Explanation of the Syntax for Mapped Types

Here's another attempt to explain roughly how the type mapping works, this time using our concrete Point type as an example. Note that the following is only an intuitive approach for explanatory purposes that doesn't accurately reflect the resolution algorithm used by TypeScript.


Let's start with a type alias:

type ReadonlyPoint = Readonly<Point>;

We can now substitute the type Point for each occurrence of the generic type T in Readonly<T>:

type ReadonlyPoint = {  
    readonly [P in keyof Point]: Point[P];
};

Now that we know that T is Point, we can determine the union of string literal types that keyof Point represents:

type ReadonlyPoint = {  
    readonly [P in "x" | "y"]: Point[P];
};

The type P represents each of the properties x and y. Let's write those as separate properties and get rid of the mapped type syntax:

type ReadonlyPoint = {  
    readonly x: Point["x"];  
    readonly y: Point["y"];
};

Finally, we can resolve the two lookup types and replace them by the concrete types of x and y, which is number in both cases:

type ReadonlyPoint = {  
    readonly x: number;  
    readonly y: number;
};

And there you go! The resulting ReadonlyPoint type is identical to the FrozenPoint type that we created manually.



More Examples for Mapped Types

We've seen the Readonly<T> type that is built into the lib.d.ts file. In addition, TypeScript defines additional mapped types that can be useful in various situations. Some examples:

/** 
* Make all properties in T optional 
*/
type Partial<T> = {  
  [P in keyof T]?: T[P];
};

/** 
* From T pick a set of properties K 
*/
type Pick<T, K extends keyof T> = { 
  [P in K]: T[P];
};

/** 
* Construct a type with a set of properties K of type T 
*/
type Record<K extends string, T> = {  
    [P in K]: T;
};

And here are two more examples for mapped types that you could write yourself if you have the need for them:

/** 
* Make all properties in T nullable 
*/

type Nullable<T> = {  
    [P in keyof T]: T[P] | null;
};

/** 
* Turn all properties of T into strings 
*/
type Stringify<T> = {  
    [P in keyof T]: string;
};

You can have fun with mapped types and combine their effects:

type X = Readonly<Nullable<Stringify<Point>>>;

// type X = {
//     readonly x: string | null;
//     readonly y: string | null;
// };


Practical Use Cases for Mapped Types

  • React: A component's setState method allows you to update either the entire state or only a subset of it. You can update as many properties as you like, which makes the setState method a great use case for Partial<T>.

  • Lodash: The pick utility function allows you to pick a set of properties from an object. It returns a new object containing only the properties you picked. That behavior can be modeled using Pick<T>, as the name already suggests.



Resource: Marius Schulz


The Tech Platform

0 comments

Recent Posts

See All

Comments


bottom of page