Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate TypeScript 4.1+ ~21 min· topic 5 of 5

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 as in 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 never doesn'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 getName for a field name.
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.

prefix.tswhole filets
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.

getters.tswhole filets
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.

Drop keys by mapping them to neverdiagram
Rendering diagram…

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. 1

    Rename with a suffix

    In the first example change the prefix template to ` ${string & K}_field . Predict the names, then hover P: they are age_field and name_field`.

  2. 2

    Keep instead of drop

    In the second example flip the test so only password is kept: K extends "password" ? K : never. Predict the type of Public: it is { password: string }.

  3. 3

    Grow the event union

    In the last example add | { type: "resize"; width: number }. The handlers object turns red. Read the error to see the new property name TypeScript expects (onResize), then add it.

Code & diagrams

Rename keys and build getters

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 function
Drop keys by mapping them to never

Public 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' }
Filter by value type: methods and data

The same test, with the branches swapped, gives the two halves of Shape. Together they cover every property exactly once.

Console

(nothing was printed)
A typed handler table from an event union

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 KeyA

Break 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>.

terminal
$ Types panel
── what you'll see ──
index.ts(8,37): error TS2344: Type 'K' does not satisfy the constraint 'string'.
Type 'keyof T' is not assignable to type 'string'.
Type 'string | number | symbol' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.

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.

terminal
$ Types panel
── what you'll see ──
index.ts(14,50): error TS2353: Object literal may only specify known properties, and 'token' does not exist in type 'OmitKeys<Account, "password" | "token">'.

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-in Omit does.

  • ▸

    Omit<T, K> in the standard library accepts any K extends keyof any, so it doesn't catch typos in K. A version built with Drop extends keyof T (like OmitKeys here) 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 infer T back from the result: TypeScript can reverse a plain homomorphic mapped type to infer T, but not one with as.

Remember this

  1. 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 (getName from name) or fewer keys (no password). TypeScript 4.1 added an as clause after the key list: { [K in keyof T as NewName]: T[K] }. For every key K the compiler works out NewName, 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. 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", and get${...} glues text around it, so the key name becomes getName`. A union of keys becomes a union of new names, one per key.

  3. 3

    Keys can be string, number or symbol, but a template literal can only hold text-like parts. Writing ` Capitalize<K> with K extends keyof T is an error, because K might be a symbol. The standard fix is string & K: the intersection keeps only the text keys. You'll see string & K` in nearly every remapping example.

  4. 4

    To drop a key, map it to never. A property whose new name is never does not appear in the result. So as K extends "password" ? never : K keeps every key except password. That K extends "password" ? ... : ... is a conditional type, "if this then that", covered in Topic 9.1. For now read it as an if.

  5. 5

    Filtering by the property's *value type* works the same way: as T[K] extends Function ? never : K drops 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 how Omit can be written without Exclude (Topic 10.1).

  6. 6

    Remapping from a union of objects is also possible. If each member has a type tag, { [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 as onClick, onKey, onScroll straight from the event union. Add an event to the union and the handler table must grow too.

  7. 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 with as.

Explain it without notes

01

What does the as clause in a mapped type do?

02

How do you remove a property from a mapped type, and how do you remove all properties whose value is a function?

03

Why do remapped types so often use string & K?

04

How would you create a typed handler table (onClick, onKey) from a union of event types?

Practice

01

Write Setters<T> that turns { name: string; age: number } into { setName: (v: string) => void; setAge: (v: number) => void }.

02

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 as in a mapped type.

  • I can drop keys by mapping them to never.

  • I can filter properties by their value type.

  • I can build getX names with Capitalize<string & K>.

  • I can say why string & K is needed.