Definition
Deriving a type from a value means you don't write the list of allowed values once as a type and once as a value. Instead you keep a single value as the source of truth and compute the type from it.
There are two tools, and they come as a set: the syntax that pulls a type out of a value (indexed access, [number]), and the mechanism that keeps that value from widening (as const, const type parameters). Use only one of them and it doesn't work.
Why you need it
Whenever two places own the same fact, they will drift apart. Take the column list of a table component as an example.
const columns = [{ id: 'orderNo' }, { id: 'total' }];
type ColumnId = 'orderNo' | 'total'; // a copy written by hand
Now say you delete the total column. You removed it from the array but the union still has it, so code that writes sort.columnId = 'total' passes with no warning at all. You only find out sorting is broken once it's running.
The reverse is just as bad: fix the union but forget the array, and the column is still on screen with no way to sort by it.
Switch to derivation and the moment you delete a column, every call site that sorted by that id becomes a compile error. That's the decisive difference from a hand-written union. A hand-written union lets you fix the union and miss the usages.
How it works
Step 1: pull the type out of the value
Indexed access types are the syntax for extracting a property's type from an object type. There's no dot notation — always brackets.
type User = { id: string; name: string };
type Id = User['id']; // string
Attach [number] to an array type and you get the union of every element type the array can hold.
type Pair = [string, number];
type Element = Pair[number]; // string | number
This is exactly why you shouldn't reach for keyof. keyof Pair gives you everything on the array itself: '0' | '1' | 'length' | 'map' | ....
Indexed access distributes over unions. (A | B)['id'] becomes A['id'] | B['id']. Put those three pieces together and you get what you want.
type ColumnIdOf<Cols extends readonly { id: string }[]> = Cols[number]['id'];
Step 2: keep the value from widening
Here's where it gets real. When TypeScript infers a type from a value, it deliberately promotes it to something looser. That's called widening.
const a = 'orderNo'; // 'orderNo' ← the variable itself keeps the literal
const obj = { id: 'orderNo' }; // { id: string } ← ⚠️ the property widens
const cols = [{ id: 'a' }, { id: 'b' }]; // { id: string }[] ← neither a tuple nor literals
Even with const, object properties widen. That's a reasonable courtesy, since you could later do obj.id = 'something else' — but it's fatal when you want an exact list.
Run the derivation we built earlier against a widened value and you get this.
type Bad = (typeof cols)[number]['id']; // string
No error, and the protection quietly drops to zero. This is the biggest trap in the pattern. The types pass, and the typo travels all the way to runtime.
Step 3: two ways to stop it
// (a) as const at the call site
const cols = [{ id: 'orderNo' }, { id: 'total' }] as const;
// (b) const type parameter (TypeScript 5.0+)
function defineColumns<const T extends readonly { readonly id: string }[]>(cols: T): T {
return cols;
}
const cols = defineColumns([{ id: 'orderNo' }, { id: 'total' }]);
With (a), if a consumer forgets as const the whole thing is defeated without a warning. With (b) the function enforces it, so there's nothing to forget. For a public API, pick (b).
You must put readonly in the constraint. The result of const inference is a readonly tuple, so if the constraint is a plain array, compilation fails with "a readonly value can't go where a mutable one is expected".
Applying it in practice
When you need to specify only some type arguments
You often want to name the row type yourself while letting the column array be inferred. The problem: TypeScript has no syntax for specifying only some type arguments.
// ❌ You can't give Row and let T be inferred
function defineColumns<Row, const T extends readonly ColumnDef<Row>[]>(cols: T): T;
defineColumns<OrderRow>(cols); // it demands T too
Split the function into two layers (currying) to separate the levels.
function defineColumns<Row>() {
return <const T extends readonly ColumnDef<Row>[]>(cols: T): T => cols;
}
const cols = defineColumns<OrderRow>()([
{ id: 'orderNo', header: 'Order No.' },
{ id: 'total', header: 'Total' },
]);
type SortKey = ColumnIdOf<typeof cols>; // 'orderNo' | 'total'
Other places the same technique works
// deriving a path union from a route list
function defineRoutes<const T extends readonly string[]>(paths: T): T { return paths; }
const routes = defineRoutes(['/home', '/settings']);
type Route = (typeof routes)[number]; // '/home' | '/settings'
// deriving a key union from a constant object
const THEME = { light: '#fff', dark: '#000' } as const;
type ThemeName = keyof typeof THEME; // 'light' | 'dark'
How to verify it
Hover over the derived type. If you see string, something widened along the way. The cause is usually one of two things.
- You parked the array literal in a variable first and then passed it
- You merged an already-widened array with a spread (
[...base, extra])
A const type parameter only preserves literals written directly at the call site. It can't resurrect a variable that has already widened to string.
Trade-offs
Derivation doesn't always win.
What you gain is a guarantee that the truth lives in exactly one place. Delete a column and every related call site turns red, so half-finished fixes become structurally impossible.
What you pay is a type signature that's harder to read. <const T extends readonly { readonly id: string }[]> scares beginners. And once currying enters the picture, even the call syntax gets unfamiliar: defineColumns<Row>()(cols).
If the list is short, rarely changes, and is used inside a single file, writing the union by hand is better. The deciding question is "when I change this list, do I have to change something else too?" If yes, derive it. If no, just write it out.
When not to use it
- When the list is decided at runtime. If the columns come down in a server response, you can't build a union at compile time. That's the territory of a runtime parser, not types.
- APIs where consumers assemble the array before passing it. If the dominant usage is building the array in a variable or merging it with a spread, a
consttype parameter can't save anything. You just end up pretending there's protection. - A short list used in one place. The signature complexity costs more than the safety you gain.
Common mistakes
- Using the derivation syntax but forgetting
const. The most common and quietest failure. The result becomesstringand no error appears. - Forgetting
readonlyin the constraint. This one fails loudly instead: you get "readonly can't be assigned to mutable". - Parking the array literal in a variable before passing it.
const raw = [...]; defineColumns(raw)is already widened and unrecoverable. It has to go directly inside the call. - Never checking whether the derivation actually works. There's just one verification question: "if I delete this column, which call site becomes a compile error?" If the answer is "nowhere", you have no protection.
- Trusting a hover and moving on. Having a human hover-check every time doesn't last. Pin it down with a type test instead.
Related concepts
- typescript-variance — why leaning on variance checks when writing constraints collapses as soon as the syntax changes
- generic-context-factory — how to carry this derivation down to the pieces of a compound component
- type-level-testing — the only way to catch a derivation that has quietly been defeated
- single-source-of-truth-content-metadata — the same principle: two owners of one fact will drift