open atlas
↑ Back to track
TypeScript type system, deep TS · 04 · 04

Key remapping via as: rename and filter keys in a mapped type

Key remapping (`as` in a mapped type, TS 4.1) computes a new key per iteration. Rename with template literals and `Capitalize`, or remap a key to `never` to delete it — how you filter properties by value type. Keys must be string|number|symbol, and `as` breaks homomorphism.

TS Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

A state-management library types your store’s actions from your state: { name: string; age: number } becomes { setName(v: string): void; setAge(v: number): void }. No code generation, no decorators — a mapped type renamed each key from name to setName and reshaped its value into a setter. The mechanism is a four-word clause inside the mapped type: as followed by a computed key. And the same as can make a key vanish.

After this lesson you’ll be able to build that rename, filter a type to data-only fields by value, and explain precisely what you give up when you reach for as — homomorphism gone, readonly dropped silently.

Renaming keys with as and template literals

When you need every key in a type to follow a naming convention — prefix with get, suffix with Changed, uppercase — you could write each one by hand. Or you can instruct the mapped type to compute the key instead of reuse it.

Since TS 4.1, a mapped type may add as <expr> after the in clause. For each K, the property is emitted under the key that <expr> resolves to, instead of under K itself. Combined with template literal types and the intrinsic Capitalize, this renames keys mechanically:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type State = { name: string; age: number };
type G = Getters<State>;
// ^? { getName: () => string; getAge: () => number }

Capitalize<"name"> is "Name" (an intrinsic string-manipulation type — unit 05’s template-literal lesson covers these in full), so the template `get${...}` yields "getName". The string & K intersection is load-bearing: keyof T can include number and symbol keys, but Capitalize only accepts string, so string & K narrows each key to its string part before interpolation.

Filtering keys: remap to never to delete them

Here is the move that makes remapping powerful. If the computed key is never, that property is omitted entirely. Pair this with a conditional on the value type and you get value-based filtering — no Pick/Omit key list required:

// Drop every property whose value is a function
type DataOnly<T> = {
  [K in keyof T as T[K] extends (...a: any[]) => any ? never : K]: T[K];
};

type Model = { id: number; name: string; save(): void; load(): void };
type D = DataOnly<Model>;
// ^? { id: number; name: string }   — save and load were remapped to never, dropped

The conditional runs per key: function-valued keys remap to never (and disappear), the rest remap to themselves (K) and survive. The inverse — keeping only keys whose value matches — just swaps the branches:

// Keep only keys whose value is a string (Pick-by-value-type)
type StringKeysOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type R = StringKeysOnly<{ a: string; b: number; c: string }>;
// ^? { a: string; c: string }
Why this works

Why does never delete the key rather than create a never-keyed property? A mapped type’s key set is the union of all computed keys, and never is the empty union — it contributes nothing to that union, so no property is generated for that iteration. It’s the same “never is the empty union” fact from the conditional-types lesson, now used as a deletion primitive. Crucially, the value T[K] of a dropped key is never evaluated into the output, so this is genuine omission, not a member with value never.

The computed key must be assignable to PropertyKey — i.e. string | number | symbol. Remap to anything else (an object, a boolean) and TypeScript errors. That’s why renames go through template literals (which produce string keys) and filters go through never (the empty key).

Remapping also breaks homomorphism (last lesson): the moment you add as, the mapped type is no longer the bare { [K in keyof T]: ... } shape, so the link to the source is severed — modifiers like readonly/? are no longer auto-copied, and arrays/tuples won’t be preserved as containers. If you need both renaming and modifier preservation, you re-apply modifiers explicitly.

The cost side: a renamed key map is a maintenance liability

Key remapping is the most magical of the mapped-type tricks, and magic has a price. The first cost is invisible breakage: because as severs homomorphism, a Getters<T> silently drops the readonly and ? modifiers the source carried. A teammate who later marks a field readonly in the state type sees that constraint vanish from the generated setters — a real bug with no error. The second cost is navigability: getName exists nowhere in the source, so Find-References, grep, and a new reader’s mental model all miss it. The generated key is undebuggable by text search.

The senior breakpoint: remapping earns its place when the rename rule is genuinely uniform and the source set is open — deriving an event-handler interface from a props type, where adding a prop must add a handler automatically. It is the wrong tool the moment the key transform encodes business meaning that a reader needs to see, or the moment the source has only a handful of fixed fields. For value-based filtering specifically, weigh it against a runtime check: if the goal is to act on data-only fields at runtime, a plain Object.entries(...).filter(...) is legible and testable, where DataOnly<T> only shifts the discrimination to compile time and produces nothing at runtime. And when an external consumer (an SDK, a public API) needs these renamed shapes, a codegen step that writes out interface StoreActions { setName(v: string): void } as real source beats a remapped type the consumer cannot grep, hover cleanly, or extend — the generated .d.ts is an artifact; the as tower is a riddle.

lesson.inset.warning

Remapping also amplifies the template-literal blow-up risk you’ll meet in the next lesson. A rename like as `${Uppercase<K>}_${Uppercase<P>}` over two key unions builds a cross product of string literals: two 50-member key sets produce a 2,500-member union in one step, and a third axis pushes you toward the 100,000-member cap and Expression produces a union type that is too complex to represent. Watch Instantiation count under tsc --extendedDiagnostics; a remap that multiplies key sets is a fast route from a snappy editor to multi-second hovers. Keep the rename’s input key sets small, or generate the names ahead of time.

Quiz

What is DataOnly<{ id: number; save(): void }> where DataOnly<T> = { [K in keyof T as T[K] extends (...a: any[]) => any ? never : K]: T[K] }?

Order the steps

Order how TypeScript resolves Getters<{ name: string }> where Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }:

  1. 1 Iterate keyof T; the only key is 'name'
  2. 2 Narrow the key with string & K to its string part, 'name'
  3. 3 Compute the new key: Capitalize<'name'> is 'Name', so the template gives 'getName'
  4. 4 Emit the property under 'getName' with value () => T['name'], i.e. () => string
Recall before you leave
  1. 01
    What does the `as` clause do in a mapped type, and how do you build a Getters<T> that renames keys?
  2. 02
    How do you filter object keys by value type, and why does remapping a key to never delete it?
  3. 03
    What two constraints govern key remapping, and what does adding `as` cost you?
Recap

The as clause turned the key side of a mapped type into a computation: rename through template literals, or delete through never to filter by value type — and you saw that this power costs you homomorphism. We leaned on Capitalize and `get${...}` here without fully explaining them. Next, template literal types make that the main event: building string types by interpolation, watching unions explode into a cross product, and using infer inside a template pattern to parse strings — extracting :id from /users/:id and splitting on delimiters. Now when you see a Getters<T> or DataOnly<T> in library code, you’ll immediately spot the as clause, trace the rename rule, and know which modifiers were silently dropped in the process.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 7 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.