A mapped type is a loop over keys that produces an object or tuple type and looks as follows:
{[PropKey in PropKeyUnion]: PropValue}
In this blog post, we examine how mapped types work and see examples of using them. Their most importing use cases are transforming objects and mapping tuples.
For showing computed and inferred types in source code, I use the npm package asserttt
– e.g.:
// Types of values
assertType<string>('abc');
assertType<number>(123);
// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
A mapped type lets us create an object by looping over a union of property keys:
type Obj = {
[PropKey in 'a' | 'b' | 'c']: number
};
type _ = Assert<Equal<
Obj,
{
a: number,
b: number,
c: number,
}
>>;
The union of property keys at the right-hand side of in
determines how many loop iterations there are. In each iteration, PropKey
refers to one of the elements of the union and a property is created:
The property key is PropKey
– by default. We’ll learn later how to override this default or completely prevent the creation of a property.
The property value is specified by the right-hand side of the colon (:
).
The most common use case for a mapped type is transforming an existing object type. Then the property keys come from the object type:
type InputObj = {
str: string,
num: number,
};
type OutputObj = {
[K in keyof InputObj]: Array<InputObj[K]>
};
type _1 = Assert<Equal<
OutputObj,
{
str: Array<string>,
num: Array<number>,
}
>>;
We can also perform that change via a generic type:
type Arrayify<Obj> = {
[K in keyof Obj]: Array<Obj[K]>
};
type OutputObj2 = Arrayify<InputObj>;
type _2 = Assert<Equal<
OutputObj2, OutputObj
>>;
We can also apply mapped types to tuples – which is more concise than using recursion:
type WrapValues<T> = {
[Key in keyof T]: Promise<T[Key]>
};
type _ = Assert<Equal<
WrapValues<['a', 'b']>,
[Promise<'a'>, Promise<'b'>]
>>;
For more information see section “Mapping tuples via mapped types” in the blog post “Computing with tuple types in TypeScript”.
In this section, we look at more examples of property keys of existing object types being transformed.
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
operator 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 of objects:
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
. Let’s use a helper function addKey()
for that purpose:
// Information we have to provide
interface InputTokenDef {
re: RegExp,
description?: string,
}
// Information addkeys() adds for us
interface TokenDef extends InputTokenDef {
key: string,
}
const tokenDefs = addKeys({
number: {
re: /[0-9]+/,
description: 'integer number',
},
identifier: {
re: /[a-z]+/,
},
} as const);
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:
assertType<
{
readonly number: {
readonly re: RegExp,
readonly description: 'integer number',
key: string,
},
readonly identifier: {
readonly re: RegExp,
key: string,
},
}
>(tokenDefs);
That means TypeScript lets us use tokenDefs.number.description
(which exists) but not tokenDefs.identifier.description
(which does not exist).
This is what addKeys()
looks like:
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);
}
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.
So far, we have only changed property keys or values of object types. In this section, we look at filtering out 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 has 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 filter out properties of the following type Obj
whose values are not 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 stringnever
– otherwiseWith 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,
};
// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
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
>>;
That’s why 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 Equal
is a hack but currently the best technique for strictly comparing types. How it works is explained in the repository of my library asserttt
.
Now we can implement IsReadonly
(based on code by github.com/inad9300
):
type IsReadonly<T extends Record<any, any>, K extends keyof T> =
Not<StrictEqual<
{[_ in K]: T[K]}, // (A)
{-readonly [_ in K]: T[K]} // (B)
>>
;
type Not<B extends boolean> = B extends true ? false : true;
We compare two objects:
K
of T
(line A).readonly
(line B).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 {}
.
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
>>;