Advanced TypeScript: Mapped Types
Welcome to the Mapped Types lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen some ways to build types out of other types. As a simple example,
A | Bbuilds a new type out of the existing typesAandB. ThePartialutility type is a more complex example: it takes an object type, then builds a new object type where every property is optional. In this lesson, we'll see howPartialactually works, and how we can build our own utility types that do similar things.We can think of
Partialas "mapping" the object type's properties into a new set of properties. For each property in the original type, we'll get a corresponding property in the partial type. This is conceptually similar to themapmethod on arrays, which transforms existing values into new values.>
[1, 2, 3].map(x => x * 2);Result:
[2, 4, 6]
This isn't just an analogy! In TypeScript, types like
Partialare called "mapped types", which we'll explore in this lesson. In a later lesson, we'll use this knowledge to reimplementPartialourselves!Our first example aggregates some data about users. We start with a regular
Userobject type. We want to build a new type,UserAggregate. It should have all of the properties thatUserhas, except that each of them is an array rather than a single value.For example, if
Userhas anemail: stringproperty, thenUserAggregateshould have anemail: Array<string>property. That allows us to create a singleUserAggregateobject with all users' emails in itsemailproperty, all users' ages in itsageproperty, etc.First, here's the code. We'll discuss the new syntax afterward.
>
type User = {email: string};type UserAggregate = {[K in keyof User]: Array<string>};const userAggregate: UserAggregate = {email: ['amir@example.com'],};userAggregate.email;Result:
['amir@example.com']
In our
UserAggregatetype, we "map over" the properties of User with[K in keyof User]. We saw thekeyoftype operator in an earlier lesson. It gives us a union of an object type's property names, each as a literal string type.For our
Usertype above,keyof Useris just'email'. In a moment, we'll extendUserto{email: string, age: number}, and at that pointkeyof Userwill be'email' | 'age'.UserAggregatehas the same property names (keys) as the originalUsertype. But in the new type, each property's type isArray<string>. OurUserAggregatetype is exactly equivalent to the type{email: Array<string>}.Because we're mapping over the object type's keys, our mapped type works equally well for one key or a hundred keys. In the next example, our object type has two properties, "email" and "age". Our mapped type changes both properties' types.
Note that the mapped type here blindly changes both properties to
Array<string>, even thoughagewas originally a number. Our age array will be['36'], not[36]. We'll fix that in a moment!>
type User = {email: stringage: number};type UserAggregate = {[K in keyof User]: Array<string>};const userAggregate: UserAggregate = {email: ['amir@example.com'],age: ['36'],};userAggregate;Result:
{email: ['amir@example.com'], age: ['36']}The
ageproperty in that type doesn't make sense. Ages are numbers, so we want our aggregateageproperty to be anArray<number>.Fortunately, our mapped type can reference each property's original type with
User[K]. We can use that to wrap the original type, likeArray<User[K]>. In the next example,Useronce again has one string property and one number property. Our mapped type wraps those original types inArray<...>, so the properties' types areArray<string>andArray<number>.>
type User = {email: stringage: number};type UserAggregate = {[K in keyof User]: Array<User[K]>};const userAggregate: UserAggregate = {email: ['amir@example.com'],age: [36],};userAggregate;Result:
{email: ['amir@example.com'], age: [36]}>
type User = {email: stringage: number};type UserAggregate = {[K in keyof User]: Array<User[K]>};const userAggregate: UserAggregate = {email: 'amir@example.com',age: 36,};userAggregate;Result:
type error: Type 'string' is not assignable to type 'string[]'.
When we add new properties to the
Usertype,UserAggregatewill automatically get corresponding properties, each of which is an array of the original property's type. We don't have to modifyUserAggregateat all. For example, if we add a new propertycatNames: Array<string>toUser, thenUserAggregateautomatically gets a new propertycatNames: Array<Array<string>>.Our mapped types above used
Kfor the key, like[K in keyof User]. Two quick notes about that.First, we capitalize
Kbecause it's also a type. As our mapped type maps over the properties,Kgets each of the individual property types as a literal string type. First it gets the literal type'email', then it gets the literal type'age'. That's why we can accessUser[K]: it's as if we're accessingUser['email'], followed byUser['age'].Second, there's nothing special about the name
K. It's just a common choice, short for "key". Another common choice isPfor "property". But we could also write[UserProp in keyof User]: Array<User[UserProp]>. This is a style decision, like variable names or function names.