const type parameters
Infer literal and readonly tuple types at the call site without making callers write as const.
function getNamesExactly<const T extends HasNames>(arg: T): T["names"]- Added in TypeScript 5.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$const | modifier | Written immediately after the opening angle bracket, before the type parameter name. It makes `const`-like inference the default for that parameter. Permitted on type parameters of functions, methods and classes; an error on interfaces and type aliases. |
$T | type parameter | The parameter whose inference is narrowed. Its constraint matters: a mutable constraint such as `string[]` defeats the modifier, because a `readonly` tuple is not assignable to it. |
Return value
Unchanged in shape — the modifier only affects how the type argument is inferred, making it as narrow as an `as const` assertion at the call site would have made it.
Errors
Nothing at runtime — the modifier is erased along with the type parameter. tsc reports error 1277 when `const` is written on the type parameter of an interface or type alias.
Changelog
- PHP 5.0Introduced. The modifier does not reject mutable values and does not make anything readonly at runtime — it changes inference only.
TypeScript 5.0 lets you write const before a type parameter name. It makes const-like inference the default for that parameter, so a caller passing { names: ["Alice", "Bob", "Eve"] } gets readonly ["Alice", "Bob", "Eve"] instead of string[] — with no as const at the call site.
When to reach for it
Library APIs where the exact values are the API: route tables, column definitions, permission lists, state machine transitions, schema builders. Anywhere you currently write "remember to add as const" in the docs.
Two things that quietly defeat it
- A mutable constraint. With
<const T extends string[]>,Tstaysstring[], becausereadonly ["a", "b", "c"]is not assignable to a mutable array. Writeextends readonly string[]. - A variable argument. The modifier only affects literal expressions written in the call. Passing an existing
const arr = ["a", "b"]still infersstring[]— the widening already happened whenarrwas declared.
What trips people up
It is not an immutability check. const on a type parameter does not reject a mutable argument and does not freeze anything; the array is still mutable at runtime. It also cannot be used on interface or type alias parameters — only functions, methods and classes.
Example
When to use it
- Let a router infer exact path segments without callers writing as const
- Keep a config object's string values as literal unions in the returned type
- Type a column or field list so downstream keys are the exact names given
- Preserve tuple length and order for a validation or schema helper
Frequently asked questions
What does const in <const T> actually do?
It makes const-like inference the default for that type parameter, so literal expressions written at the call site keep their literal and readonly tuple types instead of widening to string, number or T[]. It replaces the caller writing as const on the argument.
Why is my type still string[] even though I used a const type parameter?
Two usual causes: a mutable constraint such as extends string[], which a readonly tuple is not assignable to — use extends readonly string[] — or passing a previously declared variable rather than a literal, since the widening already happened at that variable's declaration.
Can I put const on an interface or type alias type parameter?
No. The modifier is permitted on type parameters of functions, methods and classes only; on an interface or type alias it is error 1277. Interfaces and aliases are not inference sites, so there is nothing for the modifier to narrow.
More examples
The constraint decides whether it works
All three print the same runtime values — 3, "a", 3 — which is the point: the difference is invisible at runtime and only good has the literal tuple type. readonly ["a", "b", "c"] is not assignable to string[], so the mutable constraint in fnBad forces inference back to the wide type.
// Mutable constraint: the modifier is defeated
function fnBad<const T extends string[]>(args: T): T {
return args;
}
// readonly constraint: the modifier does its job
function fnGood<const T extends readonly string[]>(args: T): T {
return args;
}
const bad = fnBad(["a", "b", "c"]); // T is string[]
const good = fnGood(["a", "b", "c"]); // T is readonly ["a", "b", "c"]
// A variable argument widens before the call, so const cannot help
const existing = ["a", "b", "c"];
const fromVariable = fnGood(existing); // T is string[]
console.log(bad.length, good[0], fromVariable.length);Deriving a union from the values passed in
T[number] turns the inferred readonly tuple into the union of its elements, so the permission names the caller passed become the only accepted arguments. Output is "true true" then "read,write,admin". Without the const modifier T would be string[] and T[number] would be plain string, accepting any typo.
function createPermissions<const T extends readonly string[]>(list: T) {
const allowed = new Set<string>(list);
return {
all: list,
can: (permission: T[number]): boolean => allowed.has(permission),
};
}
const perms = createPermissions(["read", "write", "admin"]);
// can() accepts only "read" | "write" | "admin"
console.log(perms.can("write"), perms.can("read"));
console.log(perms.all.join(","));
// perms.can("delete") is error 2345: not assignable to "read" | "write" | "admin"
Discussion