Key Remapping

The Course
•
4 min read

A template literal type builds string types using backticks and substitutions, just as a JavaScript template builds text values. The example below constructs the property names onAdded and onRemoved from event keys. This describes allowed names; it does not allocate a handler object.

Start with names, then map keys

First form just the names: a template literal type substitutes each member of a string-literal union. The type alias below describes two strings; it does not create either callback. Capitalize is a built-in type helper turning the first character into uppercase. It is not a function you call on runtime text.

Build the names firstTypeScript
type Name = "added" | "removed";

type HookName = `on${Capitalize<Name>}`;

const first: HookName = "onAdded";
const second: HookName = "onRemoved";

console.log(first, second);

Enable JavaScript to edit and type-check this example.

Check your prediction
onAdded onRemoved

Now read as inside the mapped type as “use this new property name.” This is different from the value assertion value as SomeType: key remapping does not assert that an unchecked value is safe.

A mapped type can rename each key using as. In Hooks below, K extends string ensures the key can be passed to the built-in Capitalize string transformation. The false branch is never, which drops non-string keys from this derived type. Capitalize changes the first character’s type-level spelling, not a runtime name.

Read the mapping in steps: select a key, choose its new name, and preserve the old value relationship through T[K]. This combines conditional types, literal strings and indexed access; none of these syntax pieces should be mysterious now.

Keep remapping modest. A dozen layers of generated names can make errors harder to read. If runtime code needs those new names, construct the object explicitly or write and test a runtime transformation; a mapped type alone cannot produce it.

Predict, check, and run

Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.

Predict, then runTypeScript
type Hooks<T> = {
  [K in keyof T as K extends string ? `on${Capitalize<K>}` : never]: (
    value: T[K]
  ) => string;
};

type Events = {
  added: string;

  removed: number;
};

const hooks: Hooks<Events> = {
  onAdded: title => title.toUpperCase(),
  onRemoved: count => String(count)
};

console.log(hooks.onAdded("Types"));
console.log(hooks.onRemoved(2));

Enable JavaScript to edit and type-check this example.

Check your prediction
TYPES
2

Read the compiler error

Predict the rejected relationship before checking. Read the real diagnostic and its source location; fix the contract or the value instead of hiding it with an assertion.

Find and fix the type errorTypeScript
type Labels<T> = {
  [K in keyof T as K extends string ? `${K}Label` : never]: string;
};

const labels: Labels<{ added: number }> = { added: "Added" };

console.log(labels);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

The renamed property is addedLabel. Key remapping does not accept the original key as a substitute.

Build it yourself

Exercise

Key Remapping: practise

Write Labels<T> remapping string keys to ${K}Label, with string values. Build labels for {added:number;removed:number} and print Added, Removed. Explain who constructs the runtime object.

Your workspaceTypeScript
// Start from the stated contract. Add a valid call and an edge case.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The right-hand side is string here; the values do not need T[K].

Hint 2: a first step

Use a conditional key name ending in Label, then provide the actual object literal.

Show one solution
One possible solutionTypeScript
type Labels<T> = {
  [K in keyof T as K extends string ? `${K}Label` : never]: string;
};

const labels: Labels<{
  added: number;

  removed: number;
}> = {
  addedLabel: "Added",
  removedLabel: "Removed"
};

console.log(labels.addedLabel + ", " + labels.removedLabel);

Enable JavaScript to edit and type-check this example.

Added, Removed

Trace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.

Change it and test it

Add a third event key. Observe which property becomes required. Compare an ordinary string template at runtime with the type-only template; write one sentence distinguishing them.

Was this article helpful?

Your response is saved on this device.