const type parameters to preserve literal inference in generics
Published December 1, 2026
const asConstTuple = <const T extends readonly string[]>(...values: T): T => values;
export const statuses = asConstTuple("pending", "active", "done");
export type Status = (typeof statuses)[number];
export const isValidStatus = (value: string): value is Status =>
(statuses as readonly string[]).includes(value);
export const parseStatus = (raw: string): Status => {
if (!isValidStatus(raw)) throw new Error(`Invalid status: ${raw}`);
return raw;
};What
asConstTuple's type parameter is declared <const T extends readonly string[]>. Calling it with ("pending", "active", "done") infers T as the literal tuple readonly ["pending", "active", "done"], not the widened string[] a plain <T extends readonly string[]> would infer.
Why it matters
Ordinary generic inference widens literals the moment they're captured inside an array or object literal argument — that's why calling a firstElement<T extends unknown[]>(arr: T) with ["a", "b", "c"] infers T as string[], giving you back a plain string, even though every element passed was a specific literal. Before TypeScript 5.0, the fix was pushing the burden onto the caller: wrapping the call, or an argument, in as const. The const modifier on the type parameter moves that burden to the function's author instead — every caller gets literal inference for free, with no as const anywhere at the call site.
How it works
const Ttells TypeScript to inferTas if the argument hadas constapplied, before doing anything else with it — so the rest parameter...values: Tcaptures the exact literal tuple.Status, derived from(typeof statuses)[number], is the literal union"pending" | "active" | "done"— this only works becausestatusesis a tuple of literals, not astring[].isValidStatusnarrows a loosestring(say, from user input) down toStatus, andparseStatususes that narrowing to either return a validly-typedStatusor throw.
Gotchas
consthere only affects inference at the call site — it doesn't make the parameterreadonlyat the type level beyond whatreadonly string[]already declares, and it doesn't change runtime behavior at all.- This is unrelated to the
constkeyword on variable declarations.<const T>is specifically a TypeScript 5.0+ modifier on a type parameter; using it on an older TypeScript version is a syntax error, not a silent no-op.
Related
as const— the manual, caller-side version of the same widening-prevention;consttype parameters make it automatic for anyone calling the function.- Branded types — also fight against TypeScript's default widening behavior, but for nominal distinction rather than literal preservation.