HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

36 Mapped types {[K in U]: X}

The most common way of using a mapped type is to produce a new version of an input type (usually an object type or a tuple type) by looping over its keys.

36.1 Basic mapped types

A basic mapped type looks like this (the name Key is just an example; we can use any identifier):

{
  [Key in «KeySet»]: «PropValue»
}

A mapped type creates an object type. It loops over the elements of «KeySet» and creates one property per iteration:

36.1.1 Example: finite set of keys

The following mapped type loops over a finite set of keys:

type Obj = {
  [K in 'a' | 'b' | 'c']: number
};
type _ = Assert<Equal<
  Obj,
  {
    a: number,
    b: number,
    c: number,
  }
>>;

36.1.2 Index signature vs. mapped type

To understand how an index signature is different from a mapped type, let’s first review what an index signature is.

36.1.2.1 An index signature represents a potentially infinite set of properties

This is an example of an index signature:

type StrToNum = {
  [key: string]: number, // index signature
};

An index signature represents a potentially infinite set of properties. In this case:

In other words: StrToNum is the type of objects, used as dictionaries from strings to numbers – e.g.:

const obj1: StrToNum = {};
const obj2: StrToNum = { a: 1 };
const obj3: StrToNum = { a: 1, b: 2, hello: 3 };

More information: “Index signatures: objects as dictionaries” (§18.7).

36.1.2.2 A mapped type is a type-level function

In contrast, a mapped type is a type-level function. It maps property keys to an object literal type:

type _ = [
  Assert<Equal<
    // Type-level function applied to a finite type
    {
      [K in 'a' | 'b' | 'c']: number
    },
    // Result: object literal type with normal properties
    {
      a: number,
      b: number,
      c: number,
    }
  >>,
  Assert<Equal<
    // Type-level function applied to an infinite type
    {
      [K in string]: number
    },
    // Result: object literal type with index signature
    {
      [x: string]: number
    }
  >>,
];

Note an important difference:

36.1.3 Transforming an object type via a mapped type

The most common use case for a mapped type is transforming an object type:

type Arrayify<Obj> = {
  [K in keyof Obj]: Array<Obj[K]> // (A)
};

type InputObj = {
  str: string,
  num: number,
};
type _ = Assert<Equal<
  Arrayify<InputObj>,
  {
    str: Array<string>,
    num: Array<number>,
  }
>>;

In line A, we used keyof Obj to compute the keys of Obj and iterate over them. We used the indexed access type Obj[K] and the generic type Array to define the property values.

36.1.4 Mapping preserves the kind of type (tuple, array, object, etc.)

The input of a mapped type (tuple, array, object, etc.) determines what the output looks like:

type WrapValues<T> = {
  [Key in keyof T]: Promise<T[Key]>
};

type _ = [
  // Read-only tuple in, read-only tuple out
  Assert<Equal<
    WrapValues<readonly ['a', 'b']>,
    readonly [Promise<'a'>, Promise<'b'>]
  >>,

  // Tuple labels are preserved
  Assert<Equal<
    WrapValues<[labelA: 'a', labelB: 'b']>,
    [labelA: Promise<'a'>, labelB: Promise<'b'>]
  >>,

  // Array in, Array out
  Assert<Equal<
    WrapValues<Array<string>>,
    Array<Promise<string>>
  >>,

  // ReadonlyArray in, ReadonlyArray out
  Assert<Equal<
    WrapValues<ReadonlyArray<string>>,
    ReadonlyArray<Promise<string>>
  >>,

  // Object in, object out
  Assert<Equal<
    WrapValues<{ a: 1, b: 2 }>,
    { a: Promise<1>, b: Promise<2> }
  >>,

  // Read-only properties are preserved
  Assert<Equal<
    WrapValues<{ readonly a: 1, readonly b: 2 }>,
    { readonly a: Promise<1>, readonly b: Promise<2> }
  >>,
];

36.1.5 Example: making an interface asynchronous

The generic type Asyncify<Intf> converts the synchronous interface Intf into an asynchronous interface:

interface SyncService {
  factorize(num: number): Array<number>;
  createDigest(text: string): string;
}
type AsyncService = Asyncify<SyncService>;
type _ = Assert<Equal<
  AsyncService,
  {
    factorize: (num: number) => Promise<Array<number>>,
    createDigest: (text: string) => Promise<string>,
  }
>>;

This is the definition of Asyncify:

type Asyncify<Intf> = {
  [K in keyof Intf]: // (A)
    Intf[K] extends (...args: infer A) => infer R // (B)
      ? (...args: A) => Promise<R> // (C)
      : Intf[K] // (D)
};

36.1.6 Example: adding keys to an enum object

Consider the following enum object:

const tokenDefs = {
  number: {
    key: 'number',
    re: /[0-9]+/,
    description: 'integer number',
  },
  identifier: {
    key: 'identifier',
    re: /[a-z]+/,
  },
} as const;

We’d like to avoid having to redundantly mention .key. This is what adding them via a function addKey() would look like:

const tokenDefs = addKeys({
  number: {
    re: /[0-9]+/,
    description: 'integer number',
  },
  identifier: {
    re: /[a-z]+/,
  },
} as const);

assert.deepEqual(
  tokenDefs,
  {
    number: {
      key: 'number',
      re: /[0-9]+/,
      description: 'integer number',
    },
    identifier: {
      key: 'identifier',
      re: /[a-z]+/,
    },
  }
);

assertType<
  {
    readonly number: {
      readonly re: RegExp,
      readonly description: 'integer number',
      key: string,
    },
    readonly identifier: {
      readonly re: RegExp,
      key: string,
    },
  }
>(tokenDefs);

It’s very useful that addKeys() does not lose type information: The computed type of tokenDefs correctly records where property .description exists and where it doesn’t: TypeScript lets us use tokenDefs.number.description (which exists) but not tokenDefs.identifier.description (which does not exist).

This is an implementation of addKeys():

function addKeys<
  T extends Record<string, InputTokenDef>
>(tokenDefs: T)
: {[K in keyof T]: T[K] & {key: string}} // (A)
{
  const entries = Object.entries(tokenDefs);
  const pairs = entries.map(
    ([key, def]) => [key, {key, ...def}]
  );
  return Object.fromEntries(pairs);
}

// Information we have to provide
interface InputTokenDef {
  re: RegExp,
  description?: string,
}

// Information addKeys() adds for us
interface TokenDef extends InputTokenDef {
  key: string,
}

In line A, we use & to create an intersection type that has both the properties of T[K] and {key: string}.

36.2 Changing property keys via key remapping (as)

In the key part of a mapped type we can use as to change the property key of the current property:

{ [P in K as N]: X }

In the following example, we use as to add an underscore before each property name:

type Point = {
  x: number,
  y: number,
};
type PrefixUnderscore<Obj> = {
  [K in keyof Obj & string as `_${K}`]: Obj[K] // (A)
};
type X = PrefixUnderscore<Point>;
type _ = Assert<Equal<
  PrefixUnderscore<Point>,
  {
    _x: number,
    _y: number,
  }
>>;

In line A, the template literal type `_${K}` does not work if K is a symbol. That’s why we intersect keyof Obj with string and only loop over the keys of Obj that are strings.

36.2.1 If we use key remapping, the result is always an object literal type

We have previously seen that applying a simple mapped type to a tuple produces a tuple. That changes if we do key remapping. Then the result is always an object literal type – e.g.:

type KeyAsKeyToKey<T> = {
  [K in keyof T as K]: K
};
type _ = Assert<Equal<
  // Use Pick<> because result of KeyAsKeyToKey<> is large
  Pick<
    KeyAsKeyToKey<['a', 'b']>,
    '0' | '1' | 'length' | 'push' | 'join'
  >,
  // Result is an object, not a tuple
  {
    length: 'length';
    push: 'push';
    join: 'join';
    0: '0';
    1: '1';
  }
>>;

This result reflects the actual keys of tuples. Simple map types implicitly filter those keys. For more information, see “Mapping tuples via mapped types” (§37.4).

36.3 Filtering properties

So far, we have only changed property keys or values of object types. In this section, we look at filtering properties.

36.3.1 Filtering properties via key remapping (as)

The easiest way to filter is via as: If we use never as a property key then the property is omitted from the result.

In the following example, we remove all properties whose values are not strings:

type KeepStrProps<Obj> = {
  [
    Key in keyof Obj
      as Obj[Key] extends string ? Key : never
  ]: Obj[Key]
};

type Obj = {
  strPropA: 'A',
  strPropB: 'B',
  numProp1: 1,
  numProp2: 2,
};
type _ = Assert<Equal<
  KeepStrProps<Obj>,
  {
    strPropA: 'A',
    strPropB: 'B',
  }
>>;

36.3.2 Filtering properties by filtering key unions

Before TypeScript had key remapping via via as, we had to filter the union with property keys before iterating over it with a mapped type.

Let’s redo the previous example without as: We want to only keep properties of the following type Obj whose values are strings.

type Obj = {
  strPropA: 'A',
  strPropB: 'B',
  numProp1: 1,
  numProp2: 2,
};

The following generic helper type collects the keys of all properties whose values are strings:

type KeysOfStrProps<T> = {
  [K in keyof T]: T[K] extends string ? K : never // (A)
}[keyof T]; // (B)

type _1 = Assert<Equal<
  KeysOfStrProps<Obj>,
  'strPropA' | 'strPropB'
>>;

We compute the result in two steps:

With KeysOfStrProps, it’s now easy to implement KeepStrProps without as:

type KeepStrProps<Obj> = {
  [Key in KeysOfStrProps<Obj>]: Obj[Key]
};
type _2 = Assert<Equal<
  KeepStrProps<Obj>,
  {
    strPropA: 'A',
    strPropB: 'B',
  }
>>;

36.3.3 Built-in utility type for keeping properties: Pick<T, KeysToKeep>

The following built-in utility type lets us create a new object by specifying which properties of an existing object type we want to keep:

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

We keep a subset of the properties of T by iterating over a subset K of its property keys (keyof T).

Pick is used as follows:

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

type _ = Assert<Equal<
  Pick<ObjectLiteralType, 'eeny' | 'miny'>,
  { eeny: 1, miny: 3 }
>>;
36.3.3.1 Typing a function via Pick<>

We can implement property picking at the JavaScript level (as provided by the Underscore library). Then the utility type Pick<> helps us with the return type:

function pick<
  O extends Record<string, unknown>,
  K extends keyof O
>(
  object: O,
  ...keys: Array<K>
): Pick<O, K> {
  return Object.fromEntries(
    Object.entries(object)
    .filter(
      ([key, _value]) => keys.includes(key as K)
    )
  ) as any;
}

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};
const result = pick(address, 'street', 'number');

// Correct value?
assert.deepEqual(
  result,
  {
    street: 'Evergreen Terrace',
    number: '742',
  }
);

// Correct type?
assertType<
  {
    street: string,
    number: string,
  }
>(result);
36.3.3.2 Example: removing the constraint of parameter K of Pick<T, K>

As we have seen, the parameter K of Pick<T, K> is constrained to keys of T. That prevents some useful applications – e.g.:

type Obj = {
  '0': 'a',
  '1': 'b',
  length: 2,
};
// @ts-expect-error: Type '`${number}`' does not satisfy the constraint
// 'keyof Obj'.
type _1 = Pick<Obj, `${number}`>

`${number}` is the type of all stringified numbers (see “Interpolating primitive types into template literals” (§38.2.5)). We’d like to extract all properties whose keys are elements of that type. Alas we can’t use Pick to do so. This is a version of Pick whose parameter K is not constrained:

type PickFreely<T, K> = {
  [P in K & keyof T]: T[P];
};

Note that the operation T[P] only works if P is a key of T. Therefore, the set after in must be a subset of keyof T. That’s why we used K & keyof T and not K.

With PickFreely, we can extract the properties:

type _2 = Assert<Equal<
  PickFreely<Obj, `${number}`>,
  {
    '0': 'a',
    '1': 'b',
  }
>>;

36.3.4 Built-in utility type for filtering out properties: Omit<T, KeysToFilterOut>

The following built-in utility type lets us create a new object type by specifying which properties of an existing object type we want to omit:

/**
 * Construct a type with the properties of T except for those in
 * type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Explanations:

Omit<> is used as follows:

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

type _ = Assert<Equal<
  Omit<ObjectLiteralType, 'eeny' | 'miny'>,
  { meeny: 2; moe: 4; }
>>;

36.4 Adding and removing modifiers via mapped types

In TypeScript, properties can have to kinds of modifiers:

We can add or remove these modifiers via mapped types.

36.4.1 Example: adding the optional modifier (?)

type AddOptional<T> = {
  [K in keyof T]+?: T[K]
};
type RequiredArticle = {
  title: string,
  tags: Array<string>,
  score: number,
};
type OptionalArticle = AddOptional<RequiredArticle>;
type _ = Assert<Equal<
  OptionalArticle,
  {
    title?: string | undefined;
    tags?: Array<string> | undefined;
    score?: number | undefined;
  }
>>;

The notation +? means: make the current property optional. We can omit the + but I find it easier to understand what’s going on if it’s there.

The built-in utility type Partial<T> is equivalent to our generic type AddOptional above.

36.4.2 Example: removing the optional modifier (?)

type RemoveOptional<T> = {
  [K in keyof T]-?: T[K]
};
type OptionalArticle = {
  title?: string,
  tags?: Array<string>,
  score: number,
};
type RequiredArticle = RemoveOptional<OptionalArticle>;
type _ = Assert<Equal<
  RequiredArticle,
  {
    title: string,
    tags: Array<string>,
    score: number,
  }
>>;

The notation -? means: make the current property required (non-optional).

The built-in utility type Required<T> is equivalent to our generic type RemoveOptional above.

36.4.3 Example: adding the readonly modifier

type AddReadonly<Obj> = {
  +readonly [K in keyof Obj]: Obj[K]
};
type MutableArticle = {
  title: string,
  tags: Array<string>,
  score: number,
};
type ImmutableArticle = AddReadonly<MutableArticle>;
type _ = Assert<Equal<
  ImmutableArticle,
  {
    readonly title: string,
    readonly tags: Array<string>,
    readonly score: number,
  }
>>;

The notation +readonly means: make the current property read-only. We can omit the + but I find it easier to understand what’s going on if it’s there.

The built-in utility type Readonly<T> is equivalent to our generic type AddReadonly above.

36.4.4 Example: removing the readonly modifier

type RemoveReadonly<Obj> = {
  -readonly [K in keyof Obj]: Obj[K]
};
type ImmutableArticle = {
  readonly title: string,
  readonly tags: Array<string>,
  score: number,
};
type MutableArticle = RemoveReadonly<ImmutableArticle>;
type _ = Assert<Equal<
  MutableArticle,
  {
    title: string,
    tags: Array<string>,
    score: number,
  }
>>;

The notation -readonly means: make the current property mutable (non-read-only).

There is no built-in utility type that removes readonly modifiers.

36.5 Detecting the property modifiers readonly and ? (optional)

36.5.1 Detecting if a property is read-only

This is what using a utility type IsReadonly would look like:

interface Car {
  readonly year: number,
  get maker(): string, // technically `readonly`
  owner: string,
}

type _1 = [
  Assert<Equal<
    IsReadonly<Car, 'year'>, true
  >>,
  Assert<Equal<
    IsReadonly<Car, 'maker'>, true
  >>,
  Assert<Equal<
    IsReadonly<Car, 'owner'>, false
  >>,
];

Alas, implementing IsReadonly is complicated: readonly currently does not affect assignability and cannot be detected via extends:

type SimpleEqual<T1, T2> =
  [T1] extends [T2]
    ? [T2] extends [T1] ? true : false
    : false
;
type _2 = Assert<Equal<
  SimpleEqual<
    {readonly year: number},
    {year: number}
  >,
  true
>>;

The brackets around T1 and T2 are needed to prevent distributivity.

That means that we need a stricter equality check:

type StrictEqual<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false
;
type _3 = [
  Assert<Equal<
    StrictEqual<
      {readonly year: number},
      {year: number}
    >,
    false
  >>,
  Assert<Equal<
    StrictEqual<
      {year: number},
      {year: number}
    >,
    true
  >>,
  Assert<Equal<
    StrictEqual<
      {readonly year: number},
      {readonly year: number}
    >,
    true
  >>,
];

The helper type StrictEqual is a hack but currently the best technique for strictly comparing types. How it works is explained in “How to check if two types are equal?” (§39.3).

Now we can implement IsReadonly (based on code by GitHub user inad9300):

type IsReadonly<T, K extends keyof T> =
  StrictEqual<
    Pick<T, K>, // (A)
    Readonly<Pick<T, K>> // (B)
  >
;

We compare two objects:

If the two objects are equal, then making property K readonly didn’t change anything – which means that it is already readonly.

Related GitHub issue: “Allow identifying readonly properties in mapped types”

36.5.2 Detecting if a property is optional

This is what it looks like to use a helper type IsOptional that detects if a property is optional:

interface Person {
  name: undefined | string;
  age?: number;
}

type _1 = [
  Assert<Equal<
    IsOptional<Person, 'name'>, false
  >>,
  Assert<Equal<
    IsOptional<Person, 'age'>, true
  >>,
];

IsOptional is easier to implement than IsReadonly because optional properties are easier to detect:

type IsOptional<T extends Record<any, any>, K extends keyof T> =
  {} extends Pick<T, K> ? true : false
;

How does that work? Let’s look at the results produced by Pick:

type _2 = [
  Assert<Equal<
    Pick<Person, 'name'>,
    { name: undefined | string }
  >>,
  Assert<Equal<
    Pick<Person, 'age'>,
    { age?: number | undefined }
  >>,
];

Only the latter object is assignable to the empty object {}.

36.6 Record is a mapped type

The built-in utility type Record is simply an alias for a mapped type:

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

Once again, keyof any means “valid property key”:

type _ = Assert<Equal<
  keyof any,
  string | number | symbol
>>;

These are results produced by Record:

type _ = [
  Assert<Equal<
    // Finite key type
    Record<'a' | 'b', RegExp>,
    {
      a: RegExp,
      b: RegExp,
    }
  >>,
  Assert<Equal<
    // Infinite key type
    Record<string, boolean>,
    {
      [x: string]: boolean // index signature
    }
  >>,
];

36.7 Source of this chapter