23.4 const Type Parameters (TypeScript 5.0): Inferring Literal Types in Generics
Alright, let’s talk about one of the most quietly powerful features to land in TypeScript 5.0: const type parameters. You’ve probably written a generic function and thought, “For the love of God, TypeScript, just remember the exact value I passed in.” Well, now it can. This is about telling the compiler to stop being so helpful and inferring the most specific type possible, and to start being brilliant by inferring the most literal type possible.
Think of a generic function before TypeScript 5.0. You pass it a string literal, and it immediately gets amnesia, widening it to string. It’s like you introducing your friend, “This is my brilliant friend, Alex,” and TypeScript immediately forgetting their name and just referring to them as “a human.” Technically correct, but utterly useless for any meaningful interaction.
// The Old, Helpful-but-Infuriating Way
function getOld<T>(value: T): T {
return value;
}
const oldResult = getOld("hello");
// ^? const oldResult: string
// Thanks, I hate it. I know it's a string. I wanted to know it was "hello".
Enter const type parameters. By adding the const modifier to a generic type parameter, you’re giving the compiler a direct order: “Infer this argument not as its broad type, but as its literal, readonly, or deeply const type.” It’s you saying, “No, really, pay attention to the exact thing I’m handing you.”
// The New, "I Actually Listen" Way
function getNew<const T>(value: T): T {
return value;
}
const newResult = getNew("hello");
// ^? const newResult: "hello"
// Yes! Now we're talking.
How It Actually Works (The Magic Isn’t Free)
The magic here is in the inference. When you call a function with a const type parameter, TypeScript doesn’t just look at the value; it pretends you declared it with as const. For objects and arrays, this means it infers deeply readonly tuple types and literal object properties.
function describeValue<const T>(value: T) {
// ... function body
}
const describedString = describeValue("hello"); // T is "hello"
const describedArray = describeValue([1, 2, 3]); // T is readonly [1, 2, 3]
const describedObject = describeValue({ name: "Alice", age: 30 });
// T is { readonly name: "Alice"; readonly age: 30; }
This is a game-changer. Before this, getting this level of inference required clumsy manual type assertions or redundant function overloads. Now it’s just one keyword.
The Inevitable Rough Edges (Because Nothing is Perfect)
Of course, this power comes with a few quirks you need to watch for. The designers made some choices, and, well, let’s just call them “interesting.”
First, it only works on inference. If you explicitly specify the type parameter, the const modifier is politely ignored. It’s a hint for inference, not a hard rule for the type system.
const explicit = getNew<number>(123);
// ^? const explicit: number
// We said 'number', so it gives us 'number', not '123'. Fair enough.
Second, and this is the big one, it can sometimes be too specific. This most often bites you with empty arrays. The compiler sees [] and infers it as readonly []—an empty tuple. This is 100% accurate, but often 100% useless because you can’t push anything into a readonly empty tuple.
function createArray<const T>(items: T) {
return items;
}
const myArray = createArray([]);
// ^? const myArray: readonly []
// This is now a tuple that can never have any elements. Cool and sad.
// myArray.push(1); // Error: Property 'push' does not exist on type 'readonly []'.
The best practice here? Provide a default type or a constraint if you know you’ll be working with arrays you want to modify.
// Better: Provide a default so it has something to fall back to.
function createBetterArray<const T = unknown[]>(items: T): T {
return items;
}
const betterArray = createBetterArray([]);
// ^? const betterArray: unknown[]
// Now we can work with it.
When To Use It (And When To Run Away)
This feature is an absolute no-brainer for utility functions, configuration builders, and state management libraries—anywhere you want to capture the exact structure of what a user passes in without a bunch of manual type gymnastics.
Where should you be cautious? Any function that internally mutates its arguments. Since const inference produces readonly types, trying to mutate that data will cause a type error, which is actually a good thing! It’s protecting you from yourself. If you need to mutate, you’ll need to work with a widened type within the function.
function trickyFunction<const T>(arg: T) {
// arg.push(1); // Fails if T is a readonly array
// So, do this instead if you must mutate:
const mutableArray: number[] = [...arg]; // Works if T is an array type
}
The bottom line is this: const type parameters are a massive upgrade to TypeScript’s type inference engine. They eliminate boilerplate, enable incredibly precise types, and finally let the compiler understand your intent the first time. Just be mindful of its enthusiastic literalness, especially around those pesky empty arrays, and you’ll wield this power like a pro.