10LOC
TypeScriptadvanced

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: Capitalize needs a string, but K (from keyof T) is typed as string | number | symbol in general, so string & K narrows it to the string-like part before capitalizing.
  • toGetters builds the actual object at runtime: it loops over Object.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 full Getters<T> shape until the loop finishes. This is a common, acceptable escape hatch for "build it imperatively, assert the final shape."

Gotchas

  • as remapping can also drop keys, by mapping them to never: [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 the as clause 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.