Lesson 19 · Module 3.2 — Type-level programming
[K in keyof T as NewKey] renames and filtersLast 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.
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.
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.
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.
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.)
neverThe 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.
Recall, don’t re-read. (Answers reveal on click.)
as never causes that key to…K & string needed inside Capitalize<K & string>?as in [K in keyof T as …] is, compared to x as Type…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.
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.