DEV Community

Cover image for Exploring TypeScript - Const Assertions
Zsolt Gomori
Zsolt Gomori

Posted on • Edited on • Originally published at zsgomori.dev

Exploring TypeScript - Const Assertions

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"
Enter fullscreen mode Exit fullscreen mode

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"'.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

It's equivalent to:

const MODE: {
  readonly CREATE: "CREATE";
  readonly EDIT: "EDIT";
} = {
  CREATE: "CREATE",
  EDIT: "EDIT"
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen 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";
};
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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) {
  // ...
}
Enter fullscreen mode Exit fullscreen 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.

Top comments (0)