Topic 8.5
Key Remapping and Filtering
In one line
Adding as to a mapped type lets you rename each key, drop some keys by mapping them to never, or build getter-style names from a type.
Think of it like this
A school makes name stickers for a class list. For each child it prints a sticker. Sometimes it changes the text ("Asha" becomes "Hello, Asha"), and for children who are away that day it prints no sticker at all. **A mapped type with as is that sticker machine**: for each key it decides the new name, or decides to skip it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Key remapping
- Using
asin a mapped type to choose a new name for each property, or to drop it. - Template literal type
- A type that builds text from parts, written with backticks, like `
get${Name}`. - Capitalize
- A built-in type that makes the first letter of a text type upper case:
"name"becomes"Name". - never
- The empty type: no value fits. A property named
neverdoesn't exist. - Conditional type
- A type with an if-else, written
A extends B ? X : Y. - Filtering
- Leaving out some properties by renaming their keys to
never. - string & K
- An intersection that keeps only the text keys of
K, dropping numbers and symbols. - Getter
- A function-style property that reads a value, here named
getNamefor a fieldname. - Tag (discriminant)
- A property like
type: "click"that tells the members of a union apart.
Step by step
01Rename every key
Add as followed by the new name. Here every key gets a person_ prefix. The value types are unchanged.
type Person = { name: string; age: number };
type Prefixed<T> = { [K in keyof T as `person_${string & K}`]: T[K] };
// { person_name: string; person_age: number }02Getters from a type
Capitalize the key and add get in front. Change the value to a function that returns the old type. name: string turns into getName: () => string.
Why string & K? keyof T may include numbers and symbols, and Capitalize only accepts text. The intersection with string filters them out.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Getters<Person> is { getName: () => string; getAge: () => number }03Drop keys by mapping them to never
If the new name is never, the property is gone. K extends "password" ? never : K says: if this key is password, drop it, otherwise keep its name. With a parameter for the dropped keys you get your own Omit.
04Filter by value type
Test T[K] instead of K. If the value is a function, keep the key (or drop it). One mapped type gives only the methods, another only the data.
05Names from a union of events
Each member of an event union has a type tag. Loop over the *members*, not their keys, and name each property after the tag. The value is a handler that receives exactly that member.
Try it yourself
- 1
Rename with a suffix
In the first example change the prefix template to `
${string & K}_field. Predict the names, then hoverP: they areage_fieldandname_field`. - 2
Keep instead of drop
In the second example flip the test so only
passwordis kept:K extends "password" ? K : never. Predict the type ofPublic: it is{ password: string }. - 3
Grow the event union
In the last example add
| { type: "resize"; width: number }. Thehandlersobject turns red. Read the error to see the new property name TypeScript expects (onResize), then add it.
Code & diagrams
The Types panel lists the new names TypeScript worked out, in sorted order. getEmail was never produced, so the last line is an error, and at runtime it crashes exactly as the error predicted.
Console
(nothing was printed)Expected console output
Asha 30
Uncaught TypeError: getters.getEmail is not a functionPublic lost only password. NoSecrets lost both, so the second object literal is an error: it tries to include a token that the type no longer has.
Console
(nothing was printed)Expected console output
{ id: 1, name: 'Asha' }The same test, with the branches swapped, gives the two halves of Shape. Together they cover every property exactly once.
Console
(nothing was printed)Each property is named from one member's tag, and its handler gets exactly that member, so e.x is known inside onClick. Add a fourth event to AppEvent and handlers becomes an error until you write onThat.
Console
(nothing was printed)Expected console output
click at 3 4
key KeyABreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget string & K
In the first example change Capitalize<string & K> to Capitalize<K>.
Break #2
Leak a field you meant to drop
The second example already contains the symptom: the leaked line includes token: "abc", a field NoSecrets dropped. To see the other side, remove "token" from the drop list of NoSecrets: the error on leaked goes away, but a new one appears on shown (line 13: Property 'token' is missing in type '{ id: number; name: string; }' but required in type 'OmitKeys<Account, "password">'), because token is a required field again.
Myth vs fact
Myth
as in a mapped type is a type assertion.
Fact
It is a different feature that only shares the keyword. Here it names or drops the property key; it asserts nothing.
Myth
Mapping a key to never leaves a property of type never.
Fact
The property is removed completely. A key named never is not produced at all.
Myth
Remapping removes the optional and read-only modifiers.
Fact
In a mapped type over keyof T they are kept, even with an as clause.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Key remapping was added in TypeScript 4.1 together with template literal types, and the two are designed to be used together. Before it, dropping keys needed
Pick<T, Exclude<keyof T, K>>, which still works and is what the built-inOmitdoes. - ▸
Omit<T, K>in the standard library accepts anyK extends keyof any, so it doesn't catch typos inK. A version built withDrop extends keyof T(likeOmitKeyshere) does reject names that aren't keys. Compare the two in Topic 10.1. - ▸
A remapped type is no longer a pure reflection of
T's keys, so it can't be used to inferTback from the result: TypeScript can reverse a plain homomorphic mapped type to inferT, but not one withas.
Remember this
- 1
The problem this solves: a plain mapped type (Topic 8.4) keeps every key's name and every key, but you often need different names (
getNamefromname) or fewer keys (nopassword). TypeScript 4.1 added anasclause after the key list:{ [K in keyof T as NewName]: T[K] }. For every keyKthe compiler works outNewName, and that becomes the property's name in the result. The value type is still decided by whatever follows the colon. This is key remapping. - 2
The new name usually comes from a template literal type: a type that builds text out of pieces, written with backticks like a JavaScript template string, such as `
get${Capitalize<K>}. Topic 9.4 covers them fully. For now:Capitalize<"name">is"Name", andget${...}glues text around it, so the keynamebecomesgetName`. A union of keys becomes a union of new names, one per key. - 3
Keys can be
string,numberorsymbol, but a template literal can only hold text-like parts. Writing `Capitalize<K>withK extends keyof Tis an error, becauseKmight be a symbol. The standard fix isstring & K: the intersection keeps only the text keys. You'll seestring & K` in nearly every remapping example. - 4
To drop a key, map it to
never. A property whose new name isneverdoes not appear in the result. Soas K extends "password" ? never : Kkeeps every key exceptpassword. ThatK extends "password" ? ... : ...is a conditional type, "if this then that", covered in Topic 9.1. For now read it as anif. - 5
Filtering by the property's *value type* works the same way:
as T[K] extends Function ? never : Kdrops every method and leaves only data. This is how you split an object type into its data fields and its methods, or remove everything of one kind. It is also howOmitcan be written withoutExclude(Topic 10.1). - 6
Remapping from a union of objects is also possible. If each member has a
typetag,{ [E in Event as E["type"]]: ... }makes one property per member, named by its tag. The compiler can then build a typed handler table such asonClick,onKey,onScrollstraight from the event union. Add an event to the union and the handler table must grow too. - 7
One property per remapped name: if two keys map to the same new name, their types are unioned. Optional and read-only modifiers are kept when the mapped type is homomorphic (loops over
keyof T), even withas.
Explain it without notes
What does the as clause in a mapped type do?
How do you remove a property from a mapped type, and how do you remove all properties whose value is a function?
Why do remapped types so often use string & K?
How would you create a typed handler table (onClick, onKey) from a union of event types?
Practice
Write Setters<T> that turns { name: string; age: number } into { setName: (v: string) => void; setAge: (v: number) => void }.
Write StringsOnly<T> that keeps only the properties of T whose value type is string.
Trade-offs
- ↔
Key remapping lets one source type generate several related types (getters, setters, handlers), which keeps them in sync. The cost is that the result is harder to read and error messages mention generated names.
- ↔
Removing a field from a *type* is not removing it from the *value*. If a secret must not leave your server, strip it at runtime; the type only stops your own code from using it.
Done when you can
I can rename keys with
asin a mapped type.I can drop keys by mapping them to
never.I can filter properties by their value type.
I can build
getXnames withCapitalize<string & K>.I can say why
string & Kis needed.