TypeScript 5.0 added const type parameters, a feature that solves a small but persistent annoyance: when you write a generic function and pass it a literal value, TypeScript’s default inference widens the literal types to their general form. The result is that a function which could have inferred precise literal types instead infers loose types and you have to add as const at every call site to get the precision back. The const type parameter feature pushes the as-const requirement into the function signature so callers don’t have to think about it. This article walks through what changed, when to reach for it, and the gotchas.
The widening problem
Here’s the problem in its simplest form. You write a generic function:
function pickNames<T extends readonly { name: string }[]>(items: T) {
return items.map(i => i.name);
}
const result = pickNames([{ name: 'Alex' }, { name: 'Sam' }]);
// ^? string[]
You pass in two specific objects with literal name properties. You’d think the inferred return type would be ('Alex' | 'Sam')[] — a union of the two specific names. Instead it’s string[]. TypeScript widened the literal types to string at the moment it bound them to the generic parameter, and now the precise information is gone.
Before TypeScript 5.0, the only fix was for the caller to add as const at every call site:
const result = pickNames([{ name: 'Alex' }, { name: 'Sam' }] as const);
// ^? ('Alex' | 'Sam')[]
This works but it pushes the burden onto every caller. If you’re writing a library, you can’t easily ask every consumer to remember as const. They forget. The library’s type signatures are less precise than they could be. The compromise is to give up on precision and live with string[].

The const type parameter
TypeScript 5.0’s fix is to let you mark a type parameter with the const modifier, which tells the compiler to treat the inferred type as if the caller had written as const. The same function with the new modifier:
function pickNames<const T extends readonly { name: string }[]>(items: T) {
return items.map(i => i.name);
}
const result = pickNames([{ name: 'Alex' }, { name: 'Sam' }]);
// ^? ('Alex' | 'Sam')[]
The caller didn’t change anything. But now the inferred type for T is the readonly tuple readonly [{ readonly name: 'Alex' }, { readonly name: 'Sam' }] instead of the widened array { name: string }[], and the return type derived from T preserves the literal information. The result type is the precise union.

When to reach for it
The const type parameter is the right tool when:
- You’re writing a library or shared API where you control the function signature but not the call sites, and you want callers to get precise inferred types without remembering as const.
- The function is used to define configuration or schemas at the type level — things like form definitions, route tables, validation rules, or anywhere the value being passed in is a description that you want to derive other types from.
- You’re building a fluent or chainable API where each step’s return type depends on the literal values passed to the previous step. State machines, query builders, ORMs, and similar.
Where it doesn’t help:
- Functions where the caller is going to mutate the result. const-typed inputs become readonly, so methods that mutate (push, sort, splice in place) won’t compile.
- Functions where the precise types don’t matter at the consumer side. Adding const generics for fun makes the code harder to read without buying anything.
- Functions that already accept their input in a wider type. If your signature is
function f(x: string[]), adding const to a generic does nothing because the parameter is already widened.
Real-world: a typed event emitter
The pattern that benefits most clearly from const generics is anything that takes a list of allowed values and derives a type from them. A typed event emitter:
function createEmitter<const Events extends readonly string[]>(events: Events) {
type EventName = Events[number];
const handlers = new Map<EventName, Set<(...args: unknown[]) => void>>();
return {
on(name: EventName, handler: (...args: unknown[]) => void) {
if (!handlers.has(name)) handlers.set(name, new Set());
handlers.get(name)!.add(handler);
},
emit(name: EventName, ...args: unknown[]) {
handlers.get(name)?.forEach(h => h(...args));
},
};
}
const emitter = createEmitter(['ready', 'error', 'close']);
emitter.on('ready', () => {}); // OK
emitter.on('typo', () => {}); // ❌ Type '"typo"' is not assignable
Without the const modifier on Events, the array would be inferred as string[], EventName would be just string, and the typo case would compile fine. With const, Events becomes the readonly tuple readonly ['ready', 'error', 'close'], EventName becomes the union 'ready' | 'error' | 'close', and the typo is a compile error.
Real-world: a route definition
The other common pattern is a route table where you want to derive parameter types from path strings:
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
function defineRoute<const Path extends string>(
path: Path,
handler: (params: Record<ExtractParams<Path>, string>) => void
) {
return { path, handler };
}
defineRoute('/users/:id/posts/:postId', (params) => {
console.log(params.id); // OK
console.log(params.postId); // OK
console.log(params.foo); // ❌ Property 'foo' does not exist
});
The const modifier on Path is what lets the literal string be preserved long enough for ExtractParams to walk through it character by character and pull out the named parameters. Without const, Path would be widened to string immediately and the parameter extraction would return never.
Gotchas
A few things that bite people on the first encounter:
- const on the type parameter does not make the function input readonly at the value level. The runtime value is still the same array; only the type is treated as if it were readonly. If you mutate the value at runtime, TypeScript won’t catch it (assuming you’ve cast away the readonly).
- const works with extends, but the constraint must allow for the precise type. If you write
<const T extends string[]>instead of<const T extends readonly string[]>, the readonly inferred type can’t satisfy the constraint and you get an error. Always use readonly in the constraint when adding const. - The literal preservation propagates through nested generics, but only one level. If you have a function that takes a generic that contains another generic, only the outermost layer gets const treatment. The inner layer still widens unless you add const there too.
- Editor IntelliSense in older TypeScript versions doesn’t handle const generics correctly. If you’re on a project pinned to TS 4.x and you upgrade to 5.0+ for one feature, make sure your editor’s TypeScript version is also bumped — otherwise you’ll see misleading type hints in the IDE while the compiler reports something different.
The bottom line
const type parameters are a small TypeScript feature that fix a real annoyance for library authors and anyone building configuration-style APIs. Add the const modifier when you want literal types preserved through generic inference, use readonly in the constraint, and don’t reach for it when the caller doesn’t need the precision. It’s not a feature you’ll use on every generic function, but the cases where it does fit are cases where the alternative was either pushing as const onto every caller or accepting loose types throughout the API.
