How the new const modifier for Type Parameters makes Typescript Generics much easier
Now, after this PR has been merged, it is possible to create generic functions that infer literal expressions. If this seems complicated, let me give an example: by adding a new const modifier for type parameters (inside of <T>), this becomes possible:
const identity = <const T>(t: T) => {
return t;
}
const b = identity({
a: "a",
b: "b",
c: "c"
});
// type inferred as:
const b: {
readonly a: "a";
readonly b: "b";
readonly c: "c";
}Without the const modifier, the compiler infers a more general type:
const identity = <const T>(t: T) => {
return t;
}
const b = identity({
a: "a",
b: "b",
c: "c"
});
// type inferred as:
const b: {
a: string;
b: string;
c: string;
}This can be undesirable when you want to limit the type of values of b to be only certain flags defined in the function input. In other words, as stated in the PR:
when a literal expression in an argument is contextually typed by a const type parameter, the literal expression is given the most precise type possible.
What are literal expressions?
When you initialise a variable, they are usually not constrained to the most specific type possible, because it makes sense that the values of the keys can be changed as long as they are string s.
const a = {
a: "a",
b: "b",
c: "c"
};
// type inferred as:
const a: {
a: string;
b: string;
c: string;
}The same thing applies to arrays as well — const args = [8,5] is inferred as number[] because the compiler has no idea how many or what kind of elements there are, so it is generally reasonable to infer it to be an array of any size and numerical value.
When we want the scope of our types to be narrower, we can use literal expressions. Literal expressions can be created by using an as const assertion (note that it is not a cast but an assertion, they are different), which tells the compiler to infer the narrowest type possible — the exact value of the string.
const a = {
a: "a",
b: "b",
c: "c"
} as const;
// type inferred as:
const a: {
readonly a: "a";
readonly b: "b";
readonly c: "c";
}The result is somewhat similar to how we would use enums, or union types like type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';.
This allows the compiler to verify that not only does the value of a not change, the keys and values would not change as well — in other words, constant.
Conclusion
After learning about literal expressions, it is easy to see why this new PR is so important. Especially when previously, we had to force Typescript to do type narrowing in generics using third party tools.
In many cases for type checking, it is better to have a more constrained type compared to a less constrained one, so any improvements to that is welcome.
Here’s the Typescript Playground to play with: Typescript Playground