Lesson 19 · Module 3.2 — Type-level programming

Key remapping: [K in keyof T as NewKey] renames and filters

Last lesson the mapped loop copied keys straight across. Add an as clause and the loop gets a second power: it can rewrite the key itself before placing it in the result — rename it, prefix it, or make it vanish. This is the one new piece of syntax, and once you see the pipeline it reads cleanly.

The one rule

In [K in keyof T as X], the loop still visits each key K, but the output key is X, not K. Make X a new string to rename; make X evaluate to never to drop the key entirely.

The pipeline: iterate → remap → value

Note that as here has nothing to do with the value-world x as Type assertion from Module 0. In a mapped type it is a distinct keyword (added in TS 4.1) that names a third step in the loop. Read it as three phases:

[K in keyof T  as  NewKeyExpression]:  ValueType
//   ↑                ↑                    ↑
//  iterate       remap the key        define the value

Without as the key passes through unchanged; with it, the key first runs through your expression. The expression is just a type, so anything you can compute on a key — a template literal, a conditional — can reshape it.

Rename: build a record of getters

Template-literal types let you splice a key into a new string. Combine that with Capitalize and you get the classic getX transform:

type User = { name: string; age: number };

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

type G = Getters<User>;
// resolves to:  { getName: () => string; getAge: () => number }

The key "name" becomes "getName"; the value T[K] is still read from the original key, so the getter returns the right type.

Why K & string?

keyof T is string | number | symbol — JS objects can be keyed by numbers and symbols too. Capitalize<…> only accepts string, so passing a raw K errors. K & string intersects away the non-string members, leaving just the string part. (Extract<K, string> does the same job, more verbosely.)

Drop a key: remap it to never

The filtering trick leans on one fact from Module 1: never in a union disappears ("a" | never is just "a"). So if the as expression yields never for a key, that key is removed from the output:

type Account = { name: string; age: number; password: string };

type Public<T> = {
  [K in keyof T as K extends "password" ? never : K]: T[K];
};

type Safe = Public<Account>;
// resolves to:  { name: string; age: number }   — password key dropped

Read the as expression per key: for "name" the conditional is false, so the key stays as "name"; for "password" it is true, so the key becomes never and falls out of the resulting object. This is exactly how the built-in Omit is built. Rename, filter, or both — that is the whole of the as clause.

Check yourself

Recall, don’t re-read. (Answers reveal on click.)

In a mapped type, remapping a key with as never causes that key to…
Why is K & string needed inside Capitalize<K & string>?
The as in [K in keyof T as …] is, compared to x as Type

Primary source

The official handbook section is TypeScript Handbook — Mapped Types: Key Remapping via as, which covers both renaming with template literals and filtering with never. Best way to feel it: paste Getters and Public into the TS Playground and hover the results.

Try it yourself

Unsure whether an as you see is a key remap or a value-world cast? Paste the line into the Playground and ask “type world or value world?” Next lesson (3.3) introduces conditional types and infer — the K extends "password" ? never : K machinery you just used, examined on its own.