This article was originally published at zsgomori.dev. Head over there if you like this post and want to read others like it!
The const
assertion was released with TypeScript 3.4. It's a special type of type assertion in the sense that the const
keyword is used in place of the type.
By definition, the const
assertion has the following effects:
- Literal types will not be widened.
- Object literals will get readonly properties.
- Array literals will become readonly tuples.
Here's an example:
let foo = "foo" as const;
// => yields type "foo"
Notice that TypeScript infers the most specific type possible (string literal type in this case) although we used the let
keyword in front of the foo
variable declaration. Therefore, assigning a new value to it incurs a type error:
foo = "bar";
// => Type '"bar"' is not assignable to type '"foo"'.
In contrast, if we removed the as const
assertion, its type would be widened to string:
let foo = "foo";
// => yields type "string"
foo = "bar";
// => OK
Practical Use-Case
The const assertion comes particularly in handy when mixing with object literals. Imagine a function designed to obtain some sort of data from the backend:
function fetchData(mode: "CREATE" | "EDIT") {
// the function's body is deliberately omitted for the sake of brevity
}
It has merely a single parameter, mode
β it's a union of string literal types, which can be either of type "CREATE"
or "EDIT"
.
In such cases, it's a common practice to declare an enum-style mapping object and pass one of its properties on to the function, instead of having to juggle with brittle, raw texts:
const MODE = {
CREATE: "CREATE",
EDIT: "EDIT"
};
fetchData(MODE.CREATE);
Interestingly enough, the TS compiler will yell at us with a red squiggle saying that Argument of type 'string' is not assignable to parameter of type '"CREATE" | "EDIT"'.
. If we hover over MODE.CREATE
we will discover that it's indeed of type string
. You might be wondering, why does it happen?
Generally speaking, objects are mutable constructions in JS β that is, we can freely assign a new value to any of its properties even if the object is initialised with the const
keyword:
MODE.CREATE = "create";
// => OK
If TS inferred string literal types, we would not be able to override the properties. Instead, it widens their types to string
. const
assertion to the rescue!
const MODE = {
CREATE: "CREATE",
EDIT: "EDIT"
} as const;
It's equivalent to:
const MODE: {
readonly CREATE: "CREATE";
readonly EDIT: "EDIT";
} = {
CREATE: "CREATE",
EDIT: "EDIT"
};
With that in place, the error goes away because we conform to what TypeScript expects -- a string of type "CREATE"
.
Deriving Union String Literal Type
You may have noticed that there's a correlation between the mode
function parameter and MODE
enum-like object. For instance, when it comes to extending the possible set of modes, both of them have to be touched.
Despite the fact that in this particular case it's not a huge deal, still it's a maintenance burden we would like to avoid at all costs.
Wouldn't it be neat if we could get around it by deriving the components of mode
union string literal type from the MODE
object? Turned out, we can!
type Mode = keyof typeof MODE;
Let's break it down. At type-level, the typeof
type operator returns the type of a variable or property.
const MODE = {
CREATE: "CREATE",
EDIT: "EDIT"
} as const;
type Mode = typeof MODE;
// yields π
type Mode = {
readonly CREATE: "CREATE";
readonly EDIT: "EDIT";
};
The keyof
type operator, on the other hand, expects an object type and returns a string or numeric literal union of its keys:
type Point = keyof { x: number, y: number };
// => yields π
type Point = "x" | "y";
And that's pretty much it! From now on, no matter what changes we make on MODE
, the constituents of Mode
union string literal type will always match its keys.
const MODE = {
CREATE: "CREATE",
EDIT: "EDIT",
DELETE: "DELETE"
} as const;
type Mode = keyof typeof MODE;
// => type Mode = "CREATE" | "EDIT" | "DELETE"
function fetchData(mode: Mode) {
// ...
}
Caveat: the order of
keyof
andtypeof
does matter β in fact, it wouldn't even work the other way around. The reason behind it is thatkeyof
operates only on types.
Conclusion
In this article, we learnt about const
assertions in the context of typing enum-style mapping objects. Speaking of enums, we could have achieved identical results by using the built-in enum
construct. That said, many folks frown on it because it hasn't made its way to the JavaScript standard feature set. As far as I'm concerned, choose whatever floats your boat.
Top comments (0)