Mapped types with key remapping via as
Published September 29, 2026
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
export const toGetters = <T extends object>(obj: T): Getters<T> => {
const result = {} as Getters<T>;
for (const key of Object.keys(obj) as (keyof T)[]) {
const getter = `get${String(key).charAt(0).toUpperCase()}${String(key).slice(1)}`;
(result as Record<string, () => unknown>)[getter] = () => obj[key];
}
return result;
};What
Getters<T> maps every key K of T to a new key `get${Capitalize<K>}`, whose value is a zero-argument function returning T[K]. toGetters is the runtime counterpart: it builds an object matching that shape from any plain object.
Why it matters
Before TypeScript 4.1's as clause, a mapped type could change a property's value type but not its key — { [K in keyof T]: () => T[K] } still has the same key names as T, just wrapped values. Renaming keys meant abandoning the mapped type and writing the object type by hand, which defeats the purpose of deriving it from T in the first place: it goes stale the moment T gains or loses a field. The as clause lets the key itself be a computed expression — here, a template literal built from the original key — so the derived type tracks T automatically.
How it works
`get${Capitalize<string & K>}`is the new key:Capitalizeneeds astring, butK(fromkeyof T) is typed asstring | number | symbolin general, sostring & Knarrows it to the string-like part before capitalizing.toGettersbuilds the actual object at runtime: it loops overObject.keys(obj), computes each getter's name, and wraps the corresponding value in a function.- The cast
{} as Getters<T>is doing real work — the loop assembles the object incrementally, so nothing at any single point during the loop actually has the fullGetters<T>shape until the loop finishes. This is a common, acceptable escape hatch for "build it imperatively, assert the final shape."
Gotchas
asremapping can also drop keys, by mapping them tonever:[K in keyof T as T[K] extends Function ? never : K]filters out function-typed properties, for example. Key remapping isn't only for renaming.- The capitalization here is done twice, separately — type-level (
Capitalize<...>) and runtime (.charAt(0).toUpperCase()). They have to agree, or the object's actual keys at runtime won't match what the type claims exist.
Related
Omit/Pick— narrower key transformations (dropping or selecting keys) that predate theasclause and don't need it.- Conditional types +
infer— a different mapped-type-adjacent tool, for extracting a type out of a structure rather than remapping a type's keys.