Exploring TypeScript - Const Assertions

·🍿 2 min. read

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.

Note
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 and typeof does matter — in fact, it wouldn't even work the other way around. The reason behind it is that keyof 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.


Thanks for reading the article! I genuinely hope you learned something new! Feel free to hit me up on Twitter with any questions. May I ask you to click on that little bird to share the article with your community to help spread the word? Other folks may benefit from this piece. It would mean the world to me!

Made with Gatsby, hosted on Netlify