Translation: Crazy geek
Blog.logrocket.com/const-asser…
I find the official TypeScript documentation useful, but it always feels a bit academic and dry. Every time I discover a new feature, I want to know what problem it solves rather than write a long story.
Const assertions are, in my view, the killer assertions of TypeScript 3.4 that, as I’ll explain later, allow you to dispenses with many of the string string assertions that you’re not aware of.
Const assertion
const x = { text: "hello" } as const;
Copy the code
The official document explains:
TypeScript 3.4 introduces a new construct for a literal called const assertion. Its syntax is a type assertion. When constructing a new literal expression with a const instead of a type name (e.g. 123 as const) assertion, we can signal to the language the following:
Literal types in this expression should not be extended (e.g., cannot be converted from “hello” to a string)
Object literals get read-only properties
Array literals become read-only tuples
It’s a little dull and a little messy. Let’s take it one at a time.
A literal type without type extension
Not everyone knows about type extension and is surprised when they first discover it due to some unexpected behavior.
When we declare a literal using the const keyword, the type is the text to the right of the equals sign, for example:
const x = 'x'; // x has the type 'x'
Copy the code
The const keyword ensures that no reassignment of variables occurs and only the strict type of the literal is guaranteed.
But if we use let instead of const, the variable is reassigned and the type is extended to string, as shown below:
let x = 'x'; // x has the type string;
Copy the code
Here are two different statements:
const x = 'x'; // has the type 'x'
let y = 'x'; // has the type string
Copy the code
Y is extended to a more generic type and allowed to be reassigned to other values of that type, while the variable x can only have the value of ‘x’.
With the new const function, I can do this:
let y = 'x' as const; // y has type 'x'`
Copy the code
Object literals get read-only properties
Prior to Typescript 3.4, type extension occurred in object literals:
const action = { type: 'INCREMENT',}// has type { type: string }
Copy the code
Even if we declare the action as const, we can still reassign the Type attribute, so that the attribute is extended to a string type.
This doesn’t sound very useful, so let’s move on to a better example.
If you are familiar with Redux, you may have noticed that the action variable above can be used as a Redux action. Redux is a globally immutable state store if you don’t know it. Modify the state by sending actions to so-called reducers. Reducers are pure functions that return a new updated version of the global state after each action is scheduled to reflect the changes specified in ACION.
In Redux, the standard practice is to create an operation from a function called Action Creators. The Action Creators is a pure function that returns the Redux operation object literal and all the parameters provided to the function.
An example will better illustrate the point. An application may need a global count attribute, and to update this count attribute, we can schedule an action of type ‘SET_COUNT’, which simply sets the global count attribute to a new value, which is a literal object attribute. The action Creator for this action will be a function that takes a number as an argument and returns an object with attributes of type, SET_COUNT, and payload of type number, specifying the new value of count:
const setCount = (n: number) = > {
return {
type: 'SET_COUNT',
payload: n,
}
}
const action = setCount(3)
// action has type
// { type: string, payload: number }
Copy the code
As you can see from the code shown above, the type attribute has been extended to string instead of SET_COUNT. This is not a very safe type, we can guarantee that type is a string. Each action in redux has a Type attribute, which is a string.
This is not good. If we wanted to take advantage of distinguishable unions on type attributes, prior to TypeScript 3.4 we would have declared an interface or type for each action:
interface SetCount {
type: 'SET_COUNT';
payload: number;
}
const setCount = (n: number) :SetCount= > {
return {
type: 'SET_COUNT',
payload: n,
}
}
const action = setCount(3)
// action has type SetCount
Copy the code
This does add to the burden of writing Redux Actions and reducers, but we can resolve this problem by adding a const assertion:
const setCount = (n: number) = > {
return <const> {type: 'SET_COUNT',
payload: n
}
}
const action = setCount(3);
// action has type
// { readonly type: "SET_COUNT"; readonly payload: number };
Copy the code
You’ll notice that the type inferred from setCount already has a Readonly modifier attached to each attribute, as described in the document’s bullet.
This is what happened:
{
readonly type: "SET_COUNT";
readonly payload: number
};
Copy the code
The readonly modifier is added to each literal in the action.
In Redux, we created a federation that accepts actions, which the Reducer function can do to achieve good type safety. Before TypeScript 3.4, we did this:
interface SetCount {
type: 'SET_COUNT';
payload: number;
}
interface ResetCount {
type: 'RESET_COUNT';
}
const setCount = (n: number) :SetCount= > {
return {
type: 'SET_COUNT',
payload: n,
}
}
const resetCount = (): ResetCount= > {
return {
type: 'RESET_COUNT',}}type CountActions = SetCount | ResetCount
Copy the code
We created two interfaces RESET_COUNT and SET_COUNT to classify the return types of the two resetCount and setCount.
CountActions is the union of these two interfaces.
With const assertions, we can eliminate the need to declare these interfaces by using a combination of const, ReturnType, and Typeof:
const setCount = (n: number) = > {
return <const> {type: 'SET_COUNT',
payload: n
}
}
const resetCount = (a)= > {
return <const> {type: 'RESET_COUNT'}}type CountActions = ReturnType<typeof setCount> | ReturnType<typeof resetCount>;
Copy the code
We infer a good action union from the return types of the Action Creator functions setCount and resetCount.
Array literals become read-only tuples
Prior to TypeScript 3.4, a literal array was declared to be extended and modifiable.
Using const, we can lock a literal to its explicit value and not allow modification.
If we had a redux action type for setting the hour array, it might look something like this:
const action = {
type: 'SET_HOURS',
payload: [8.12.5.8],}// { type: string; payload: number[]; }
action.payload.push(12) // no error
Copy the code
Prior to TypeScript 3.4, extensions made the literal properties of the above operations more generic because they could be modified.
If we apply const to object literals, we have good control over everything:
const action = <const> {type: 'SET_HOURS',
payload: [8.12.5.8]}/ / {
// readonly type: "SET_HOURS";
// readonly payload: readonly [8, 12, 5, 8];
// }
action.payload.push(12); // error - Property 'push' does not exist on type 'readonly [8, 12, 5, 8]'.
Copy the code
What happens here is exactly the point of the document:
The payload array is indeed a “read-only” tuple of [8,12,5,8] (although I didn’t see that in the documentation).
conclusion
I summarize all of this with the following code:
let obj = {
x: 10,
y: [20.30],
z: {
a:
{ b: 42}}}as const;
Copy the code
Corresponds to:
let obj: {
readonly x: 10;
readonly y: readonly [20.30];
readonly z: {
readonly a: {
readonly b: 42;
};
};
};
Copy the code
Here, I can infer the type instead of writing redundant boilerplate types. This is especially useful for Redux.