skip to content

In a TypeScript mapped type, what does the `as` clause in `{ [K in keyof T as NewKey]: T[K] }` do, and what happens to a key whose `as` clause evaluates to `never`?

level: middleimportance: should knowfreq 45%

answer

  1. it renames while it iterates
  2. key position, not the value position
  3. one magic type makes keys disappear
  4. never in the as clause
  5. no property is emitted at all

basics

~20 s

The as clause renames each key while the mapped type iterates, emitting the property under the computed name instead of K. A key whose clause evaluates to never produces no property at all, which is how mapped types filter keys out.

solid answer

~50 s

A mapped type walks a union of keys and emits one property per key. The `as` clause sits in the key position and computes a *new* name for each iteration, so `{ [K in keyof T as NewKey]: T[K] }` emits the property under `NewKey` instead of under `K`. The name is the only thing it changes: `K` stays bound to the original key everywhere else, so the value position can still say `T[K]`. The result of the clause has to be a valid property key — `string | number | symbol` — and `never` is the degenerate case: there is no key to emit, so the property is dropped entirely. That is the filtering idiom, e.g. `[P in keyof T as P extends 'id' ? never : P]` is an Omit-style helper. Note this `as` has nothing to do with the `as` type assertion beyond the keyword.

code

typescript · 7 lines
typescript
type Data = { id: number; name: string; save(): void };

type Fields = {
  [K in keyof Data as Data[K] extends Function ? never : K]: Data[K];
};

const key: keyof Fields = "name"; // "id" | "name" — save was dropped

go deeper

for a junior

Recognise the syntax when you read it: the part after as is the name the property will have, and never there means the property is not emitted. You will not usually be asked to write one yet.

for a middle

Be ready to write a remapping by hand and to explain that the clause runs per key, that K still means the original key so T[K] is available, and that the result must be a valid property key.

for a senior

Show the judgment about where the conditional belongs — key position to delete, value position to retype — and be explicit that the whole construct is erased, so it describes a mapper rather than performing one.

for a principal

Own the question of how far a codebase should push derived key sets: it removes drift but produces names that appear in no source file, so weigh discoverability and error-message quality against single-source-of-truth.

## The shape being described A mapped type builds an object type by iterating a union of keys: `{ [K in Keys]: ValueType }`. When `Keys` is `keyof T`, you are saying "one property for every property of `T`". By default the emitted property keeps the key it was iterated with. The `as` clause is an optional extra step in the *key position* that overrides that name: ```ts type Renamed = { [K in keyof T as NewKeyExpression]: ValueExpression }; ``` For every `K` in the union the compiler evaluates `NewKeyExpression` and emits the property under that computed name. Nothing else about the mapped type changes. One naming trap first: this `as` is unrelated to the `as` *type assertion* (`json as User`). They share a keyword and nothing else. The assertion tells the checker to stop arguing about an expression; the mapped-type clause computes a key. Neither one does anything at runtime, but they are different features. ## Renaming The clause is an ordinary type expression, so anything that produces a key type works — including a lookup into another type: ```ts type Wire = { user_id: number; created_at: string }; type Names = { user_id: "userId"; created_at: "createdAt" }; type Domain = { [K in keyof Wire as Names[K]]: Wire[K] }; // { userId: number; createdAt: string } ``` The computed result must be assignable to `string | number | symbol` (the built-in alias `PropertyKey`). Produce something that is not a key type and the compiler reports exactly that. ## `never` deletes the property `never` is the empty type — there is no key to emit — so the property does not appear in the result at all. This is the single most-asked fact about key remapping: ```ts type Data = { id: number; name: string; save(): void }; type Fields = { [K in keyof Data as Data[K] extends Function ? never : K]: Data[K]; }; // keyof Fields is "id" | "name" ``` The result is **not** `{ save: never }`. That is what you get if you put the conditional in the *value* position instead — the key survives with an uninhabitable type, which is worse than useless because the property still shows up in `keyof`, in destructuring, and in excess-property messages. Key position removes; value position replaces. ## `K` always means the original key Inside both the `as` clause and the value expression, `K` is bound to the key being iterated, never to the renamed one. That is why filtering by *value* type works: the clause can inspect `T[K]` while deciding what to call — or whether to emit — the property. The new name is write-only; nothing downstream in the same mapped type can read it. ## The Omit-shaped helper Before key remapping existed, subtracting keys was expressed by building the surviving key union first and then `Pick`ing with it — which is how the built-in `Omit<T, K>` is defined, as `Pick<T, Exclude<keyof T, K>>`. With remapping you can write it in one pass: ```ts type Without<T, K extends PropertyKey> = { [P in keyof T as P extends K ? never : P]: T[P]; }; type NoId = Without<{ id: number; label: string }, "id">; // { label: string } ``` Both forms are legitimate; the remapping form reads better when the predicate is about the *property*, not just the name, because `T[P]` is in scope. ## Two practical cautions First, `keyof T` is `string | number | symbol` in the general case. Any operation that only accepts string keys needs the key narrowed with `K & string`, otherwise the clause is a type error on the symbol member. Second, remember what tier you are working on. A key-remapped type is a *description*: the compiler emits no code for it, so nothing in the running program is renamed, filtered, or checked. A type that says a payload has `userId` does not make the parsed JSON have `userId` — only a mapping function does that. Key remapping is excellent at stating the contract that such a function must satisfy, and useless as a substitute for it.

  • What does the compiler do if the `as` clause produces a type that is not a valid property key?
    It errors — the clause's result must be assignable to `string | number | symbol`, the built-in `PropertyKey`. `never` is the one accepted degenerate case, and it emits no property rather than an error. So a clause returning, say, `boolean` is rejected, while a clause returning `never` silently drops the key.
  • What is the difference between putting a conditional in the `as` clause and putting it in the value position?
    The key position controls *whether* a property exists: `never` there removes it. The value position controls *what type* it has: a `never` there leaves the key present with an uninhabitable type, so it still appears in `keyof`, in destructuring, and in error messages. Filtering belongs in the `as` clause.
  • Inside the value expression of a remapped mapped type, does `K` refer to the new key or the old one?
    The old one. `K` stays bound to the key being iterated everywhere in the mapped type, so `T[K]` is the original property's type. The computed name only labels the emitted property; nothing in the same mapped type can read it back.

It is a rename step on a conveyor belt: each property passes through, gets a label printed for it, and a label of never means the item is pushed off the belt entirely.

saying these in an interview costs you the question

  • Says a never key becomes a property of type never
  • Thinks the mapped-type as is the same as a type assertion
  • Believes remapping renames the object's keys at runtime
  • Puts the filtering conditional in the value position
  • Claims the as clause can produce any type, not just key types

context