Force literal-type inference in generic functions without burdening the caller
Since TypeScript 5.0, type parameters can carry the const modifier. This makes the compiler automatically infer the narrowest possible literal types on every call, without the caller having to write as const.
Table of Contents
- 1. The Problem With Default Inference
- 2. Syntax Basics
- 3. Tuple Returns and Function Parameters
- 4. const Type Parameters With Object Literals
- 5. Combining With satisfies
- 6. Limitations and Constraint Behavior
- 7. Comparison to Manually Writing as const
- 8. Best Practices
- 9. Common Pitfalls
- 10. Summary
- 11. FAQ
1. The Problem With Default Inference
Generic functions normally infer their type parameter from the argument that gets passed in. For objects and arrays, the compiler defaults to the more general type. An array of string literals becomes string[], and an object field holding "GET" becomes string.
Until now, the caller had to step in and mark the argument with as const to preserve the narrower literal types. That works, but it shifts responsibility onto every single call site and is easy to forget, especially in larger codebases with many call sites.
const type parameters flip that responsibility around: the function itself decides that its type parameter is always inferred with the precision of as const, regardless of whether the caller remembers to do anything.
2. Syntax Basics
The modifier is written directly before the type parameter's name: function f<const T>(x: T). On a call, inference for T then behaves as if the argument had implicitly been marked with as const.
In the example below, a function without the const modifier returns a broadly typed array, while the version with the const modifier preserves the concrete literal type of every element.
It's worth noting that the const modifier doesn't change which arguments are accepted, only how precise the inferred type turns out to be. A function with a const type parameter still accepts exactly the same values as its version without the modifier.
function first<T>(arr: T[]): T {
return arr[0];
}
function firstConst<const T>(arr: T[]): T {
return arr[0];
}
const a = first(["GET", "POST"]); // type: string
const b = firstConst(["GET", "POST"]); // type: "GET" | "POST"
3. Tuple Returns and Function Parameters
A classic use case involves functions that expect or return a tuple with a fixed order and a fixed type at each position, such as coordinates or ranges. Without a const type parameter, a passed array is easily widened to a general array type.
With the const modifier, the tuple structure along with its positional information is preserved, without the caller having to supply an explicit tuple or as const.
function range<const T extends readonly [number, number]>(pair: T): T {
return pair;
}
const r = range([10, 20]);
// r has the type readonly [10, 20], not number[]
4. const Type Parameters With Object Literals
The effect is just as visible with object arguments. Pass a configuration object to a generic function with a const type parameter, and every field stays fixed to its exact literal value, as if the argument had itself been marked with as const.
This is especially useful for utility functions that accept configuration objects and process them type-safely elsewhere, for example in event systems or state machines.
function define<const T extends Record<string, unknown>>(config: T): T {
return config;
}
const state = define({
status: "idle",
retries: 0,
});
// state.status has the type "idle", not string
5. Combining With satisfies
const type parameters and the satisfies operator solve a similar problem from different angles: satisfies applies when a single value is created, while const type parameters apply to the signature of a reusable function. In practice the two techniques combine well, for instance when a function with a const type parameter returns an object that the caller still wants to check against another interface.
For library authors this means: anyone designing an API that many consumers call with different literal values should consider const type parameters instead of forcing every consumer to write as const.
This combination also cuts down on the documentation an API needs, since the signature itself already communicates that precise literal types are to be expected, rather than only mentioning it in a comment or in separate external documentation.
6. Limitations and Constraint Behavior
const type parameters only change the kind of inference, not the type parameter's constraints themselves. A const T extends string[] still only allows arrays of strings, but within that boundary it infers the narrowest possible literal types.
It's also important to note: const type parameters don't turn the value into an immutable object at runtime. Just like as const, this is a pure type-system feature. Anyone who needs actual runtime immutability still has to use Object.freeze or an equivalent mechanism.
function pick<const T extends string>(value: T): T {
return value;
}
const method = pick("DELETE"); // type: "DELETE"
// method is not frozen at runtime, only the type is precise
7. Comparison to Manually Writing as const
Before TypeScript 5.0, every caller who needed literal precision had to mark their own argument with as const. That scatters responsibility across the whole codebase and is error prone, because a forgotten as const silently leads to wider types without the compiler reporting any error.
const type parameters move that decision to the function's definition site. The API signature makes it immediately obvious to every consumer that precise literal types are to be expected.
Migrating an existing library to this pattern usually happens without a breaking change, since const type parameters only refine inference and add no additional constraints on the arguments callers are allowed to pass in.
8. Best Practices
const type parameters are a great fit for factory functions, builder patterns, event definitions, and any place where the concrete literal values are later needed for discriminated unions or switch statements.
For functions whose return values are never checked against concrete literals anyway, such as pure calculation functions, the const modifier brings no benefit and should be left out to avoid unnecessarily complicating the signature.
A sensible starting point for existing projects is to first identify the most frequently called generic utility functions and trial const type parameters there, before establishing the technique as a project-wide convention.
9. Common Pitfalls
A common misconception is that const type parameters change something at runtime. The feature operates purely at the level of type inference during compilation and has no effect whatsoever on the generated JavaScript code.
Another pitfall: const type parameters require at least TypeScript 5.0. In projects running an older compiler version, the syntax causes an error. Also, overly aggressive use of const type parameters in public APIs can leave callers with unexpectedly narrow return types that cause incompatibilities elsewhere when a broader type was expected.
| Aspect | Without const modifier | Manual as const at the call site | const type parameter |
|---|---|---|---|
| Responsibility lies with | Compiler default | Caller | Function definition |
| Literal types preserved | No | Yes | Yes |
| Can be forgotten | Not applicable | Yes | No |
| Minimum version | All versions | All versions | TypeScript 5.0+ |
| Runtime effect | None | None | None |
Mironsoft
TypeScript migration, type safety, and team onboarding
A JavaScript codebase without type safety, but no time for a full migration?
We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.
Migration Roadmap
Plan and execute a gradual JS-to-TS migration without big-bang risk.
Strict Mode Rollout
Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.
Team Onboarding
Bring developers up to speed on TypeScript best practices with workshops and reviews.
10. Summary
const Type Parameters
Available since
TypeScript 5.0
Affects
Type inference at call sites
Runtime effect
None, pure type-system feature
Typical use
Factories, builders, event definitions