Section 2 · Generics in Depth
How Inference Works, and const Type Parameters
20 min3 min readCode playground
Lesson at a glance
Predict what TypeScript infers for a type parameter, keep literal types with constraints and const type parameters, and order callback arguments so inference flows the right way.
- 1Inference sites and candidates
- 2When literals widen
- 3const type parameters
- 4Callbacks are inferred last
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
In the last lesson you wrote sortByTotal(orders) and TypeScript worked out that T was DetailedOrder. Most of the time inference does what you want, which makes the times it does not feel random. They are not. TypeScript follows a small set of rules, and once you know them you can design signatures that infer exactly the type you intend, with no <…> at the call site.
Inference sites and candidates
Every place a type parameter appears inside a parameter’s type is an inference site. At a call, TypeScript matches each argument against its parameter type and collects a candidate for the type parameter from each site. Then it picks one type from the candidates.
function same<T>(a: T, b: T): T[] {
return [a, b];
}
same('processing', 'shipped'); // T = string, so string[]
same(1, 'a');
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.With two string literals, the candidates 'processing' and 'shipped' are combined and then widened to string, because T is only returned inside an array (the next section explains when literals survive). With 1 and 'a', TypeScript does not invent number | string; it picks the first candidate, number, and then the second argument fails against it. That is deliberate. Silently widening to a union would hide the bugs generics are meant to catch. If you really want mixed values, say so: same<number | string>(1, 'a').
When literals widen
The second rule explains most “why did I get string?” moments. A literal candidate like 'processing' is kept or widened depending on the signature:
function identity<T>(value: T): T { return value; }
function box<T>(value: T): { value: T } { return { value }; }
function boxStatus<T extends string>(value: T): { value: T } { return { value }; }
const a = identity('processing'); // 'processing'
const b = box('processing'); // { value: string }
const c = boxStatus('processing'); // { value: 'processing' }The literal survives when T is returned directly at the top level (identity), or when T’s constraint includes a primitive type such as string (boxStatus). Otherwise it widens, on the theory that a value tucked into an object will probably be reassigned later.
Objects follow the same logic, and here a constraint has a second benefit: it gives the argument a contextual type.
type OrderStatus = 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';
function draft<T>(order: T): T { return order; }
function draftOrder<T extends { status: OrderStatus }>(order: T): T { return order; }
draft({ status: 'processing' }); // { status: string }
draftOrder({ status: 'processing' }); // { status: 'processing' }Because the constraint says status is an OrderStatus, TypeScript checks the literal against that union while inferring, and the literal type sticks. A good constraint both documents the function and improves what it infers.
const type parameters
Sometimes you want the caller’s argument exactly as written: tuple order, literal values, nested objects. Before TypeScript 5.0 that meant asking every caller to write as const. Now the function can ask for it:
function defineFlow<const T extends readonly OrderStatus[]>(steps: T): T {
return steps;
}
const happyPath = defineFlow(['processing', 'shipped', 'delivered']);
// readonly ['processing', 'shipped', 'delivered']The const modifier tells TypeScript to infer the argument as if it had as const: literals stay literal, arrays become readonly tuples, object properties become readonly. Pair it with a readonly array constraint, since as const produces readonly arrays. It only affects literals written directly in the call; a variable you pass in keeps whatever type it already had.
Callbacks are inferred last
Arguments that are functions with unannotated parameters, such as (d) => d.totalCents, are context-sensitive: their parameter types come from the signature, which may still depend on T. TypeScript infers from everything else first, then types these callbacks. Inside a single object literal, it works through context-sensitive properties from left to right:
function createStep<T>(config: { load: (orderId: number) => T; render: (data: T) => string }) {}
createStep({
load: (orderId) => ({ orderId, totalCents: 4500 }),
render: (d) => d.totalCents.toFixed(), // d: { orderId: number; totalCents: number }
});
createStep({
render: (d) => d.totalCents.toFixed(), // Error: 'd' is of type 'unknown'.
load: (orderId) => ({ orderId, totalCents: 4500 }),
});When render comes first, T has no candidate yet. You can fix it at the call site by swapping the properties or annotating orderId: number, which makes load no longer context-sensitive. Better, fix it in the API: put the producer before the consumer in your parameter lists and documented examples, so the natural way to call it is the way that infers.
In the exercise you will make three Cartwheel helpers keep the literals their callers pass. Next, you will learn to stop inference from happening where it should not.
Key takeaways
- Every place a type parameter appears in a parameter type is an inference site; TypeScript collects a candidate from each argument and picks one type.
- Inferred literals widen unless the type parameter’s constraint includes a primitive type or the parameter is declared
const. - A
consttype parameter infers arguments as if they were written withas const, keeping literals and readonly tuples without asking the caller. - Callbacks with unannotated parameters are inferred after other arguments, and inside one object literal TypeScript works left to right.
- Explicit type arguments are all or nothing; specify one and TypeScript stops inferring the rest.