Advanced · 18 min
Designing generic APIs
Use sensible generic defaults and preserve types through a reusable store.
Offer a useful default
A default type parameter makes a common use concise while allowing callers to choose a different error shape. Required type parameters come first. The union still needs narrowing before reading a property from one variant.
type Result<T, E = string> = { ok: true; value: T } | { ok: false; error: E };
const result: Result<number> = { ok: false, error: "Retry" };
function describe(result: Result<number>): string {
return result.ok ? String(result.value) : result.error;
}
console.log(describe(result));Output
Retry
Preserve one type across operations
The store factory infers one type and uses it for both reads and writes. A generic API should preserve a useful relationship, not merely add type parameters. This small in-memory store returns its stored reference; it does not clone or persist data.
interface Store<T> { read: () => T; write: (value: T) => void }
function createStore<T>(initial: T): Store<T> {
let current = initial;
return { read: () => current, write: value => { current = value; } };
}
const store = createStore({ name: "Ada", points: 1 });
store.write({ name: "Ada", points: 4 });
console.log(store.read().points);Output
4
Put it into practice
- Predict the output and explain which relationships the types enforce.
- Fix the task while preserving its type contract, then check both practice questions.
Try it yourself
Code runs on this device. When you are signed in, drafts sync to your account.
Keep store writes compatible
Write numeric points with value 4. Keep the inferred store type and the factory.
The initial record determines the type expected by later writes.
Show solution
interface Store<T> { read: () => T; write: (value: T) => void }
function createStore<T>(initial: T): Store<T> {
let current = initial;
return { read: () => current, write: value => { current = value; } };
}
const store = createStore({ name: "Ada", points: 1 });
store.write({ name: "Ada", points: 4 });
console.log(store.read().points);The inferred type is shared by the read and write functions. A numeric value matches the original shape without weakening the generic API.