A registry is an object whose keys identify operations. A mapped contract can connect every key to its particular input type: Formatters<T> requires one formatter for each property in T, with that property’s own value type.
Using satisfies on an explicit object checks that coverage and contextually types the callbacks. The title callback receives string while pages receives number. A broad (value: string | number) => string everywhere would lose that useful relationship.
Types describe coverage, but runtime code still supplies every implementation. This course deliberately builds small explicit registries rather than casting the output of Object.keys into a generic mapped type. Object.keys returns runtime strings and does not prove every required function was constructed.
When an event type is added, a complete registry should fail checking until its implementation exists. That maintenance signal is valuable only if you do not silence it with a broad assertion. The final gateway uses the same idea for its channels.
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.
type Formatters<T> = { [K in keyof T]: (value: T[K]) => string };
type Book = {
title: string;
pages: number;
};
const format = {
title: text => text.trim(),
pages: count => `${count} pages`
} satisfies Formatters<Book>;
console.log(format.title(" Types "));
console.log(format.pages(120));Enable JavaScript to edit and type-check this example.
Check your prediction
Types
120 pages 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.
type Formatters<T> = { [K in keyof T]: (value: T[K]) => string };
const format = { pages: (count: string) => count } satisfies Formatters<{
pages: number;
}>;
console.log(format);Enable JavaScript to edit and type-check this example.
Check your diagnosis
The pages callback must accept number. A registry is not safe merely because its keys are present.
Coverage and payload compatibility are separate checks
A mapped registry has two responsibilities: every required key exists, and each implementation accepts the payload associated with its key. A broad function union can lose the second relationship even while all names appear to be present.
Construct a small literal satisfying the derived contract and inspect the callback parameter types. Context lets implementations remain concise without sacrificing their specific input shapes. The runtime object still has to be written or generated by real code; a mapped alias cannot manufacture it.
type Fields = {
title: string;
pages: number;
};
type Formatters<T> = { [K in keyof T]: (value: T[K]) => string };
const format = {
title: value => value.trim(),
pages: value => `${value} pages`
} satisfies Formatters<Fields>;
console.log(format.title(" Types "));
console.log(format.pages(0));Enable JavaScript to edit and type-check this example.
Check your prediction
Types
0 pages Each callback receives a different type. The pages formatter can use zero as meaningful data without a truthiness fallback.
type Events = {
added: { title: string };
removed: { id: string };
};
type Formatters<E> = { [K in keyof E]: (value: E[K]) => string };
const format = {
added: value => value.title,
removed: value => value.id
} satisfies Formatters<Events>;
console.log(Object.keys(format).join(","));
console.log(format.removed({ id: "b1" }));Enable JavaScript to edit and type-check this example.
Check your prediction
added,removed
b1 The explicit registry implements both derived requirements. Adding a key to Events will demand another implementation; it will not add one to this runtime object automatically.
Try the rule on new inputs
Remove one key and then change one callback input independently. Read the different diagnostics. Keep a positive call for each key so an over-restrictive contract cannot pass a negative-only test suite.
Build it yourself
Typed Registries: practise
Create Formatters for {enabled:boolean;minutes:number}. Format false as Off and 25 as 25 min. Use satisfies, with no explicit callback parameter annotations and no any.
// 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
Context comes from the mapped contract at the object boundary.
Hint 2: a first step
Keep a formatter under each original key and branch on the boolean input.
Show one solution
type Formatters<T> = { [K in keyof T]: (value: T[K]) => string };
const format = {
enabled: value => (value ? "On" : "Off"),
minutes: value => `${value} min`
} satisfies Formatters<{
enabled: boolean;
minutes: number;
}>;
console.log(format.enabled(false));
console.log(format.minutes(25));Enable JavaScript to edit and type-check this example.
Off
25 minTrace 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
Remove minutes and inspect the missing-property error. Then call the enabled formatter with text and inspect the argument error. These protect different parts of the contract.
Completion is saved in this browser only. It does not measure mastery.