10LOC
TypeScriptadvanced

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 T tells TypeScript to infer T as if the argument had as const applied, before doing anything else with it — so the rest parameter ...values: T captures the exact literal tuple.
  • Status, derived from (typeof statuses)[number], is the literal union "pending" | "active" | "done" — this only works because statuses is a tuple of literals, not a string[].
  • isValidStatus narrows a loose string (say, from user input) down to Status, and parseStatus uses that narrowing to either return a validly-typed Status or throw.

Gotchas

  • const here only affects inference at the call site — it doesn't make the parameter readonly at the type level beyond what readonly string[] already declares, and it doesn't change runtime behavior at all.
  • This is unrelated to the const keyword 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; const type 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.