{[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.
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:
Key
is the current element of «KeySet»
. It determines the property key. (There are ways to use a different key or to skip a property. We’ll explore those later.)
Key
.
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,
}
>>;
To understand how an index signature is different from a mapped type, let’s first review what an index signature is.
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:
The binding identifier key
is ignored and can be any identifier. It distinguishes an index signature from a property with a computed key.
Each property has a string
key. The property type must be infinite – e.g.: string
, number
, symbol
, a template string literal with an infinite primitive type (such as `${bigint}`
)
Each property has a value of type number
.
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).
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:
[K in string]
: K
is a type variable that can be used after the colon.
[x: string]
: x
doesn’t provide any functionality (it only serves as a syntactic marker), but it is similar to a normal variable.
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.
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> }
>>,
];
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)
};
Intf
(line A).
K
we check if the property value Intf[K]
is a function or method (line B).
infer
keyword to extract the arguments into the type variable A
and the return type into the type variable R
. We use those variables to create a new property value where the return type R
is wrapped in a Promise (line C).
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}
.
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.
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).
So far, we have only changed property keys or values of object types. In this section, we look at filtering properties.
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',
}
>>;
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:
K
is mapped to:
K
– if the property value T[K]
is a string
never
– otherwise
never
disappears when we do that.
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',
}
>>;
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 }
>>;
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);
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',
}
>>;
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:
K extends keyof any
means K
must be a subset of all possible property keys:
type _ = Assert<Equal<
keyof any,
string | number | symbol
>>;
Exclude<keyof T, K>>
means: take the keys of T
and remove all “values” mentioned in K
.
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; }
>>;
In TypeScript, properties can have to kinds of modifiers:
?
readonly
We can add or remove these modifiers via mapped types.
?
)
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.
?
)
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.
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.
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.
readonly
and ?
(optional)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:
K
of T
(line A)
readonly
(line B)
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”
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 {}
.
Record
is a mapped typeThe 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
}
>>,
];