Exploring TypeScript - Const Assertions
Tags: typescript, webdev, programming
Published: 2021-05-09 12:16:39

*This article was originally published at [zsgomori.dev](https://www.zsgomori.dev/blog/caching-fonts-on-netlify/). Head over there if you like this post and want to read others like it!*

The `const` assertion was released with [TypeScript 3.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions). 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:

```ts
let foo = "foo" as const;
// => yields type "foo"
```

Notice that TypeScript infers the most specific type possible ([string literal type](https://mariusschulz.com/blog/string-literal-types-in-typescript) 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:

```ts
foo = "bar";
// => Type '"bar"' is not assignable to type '"foo"'.
```

In contrast, if we removed the `as const` assertion, its type would be [widened](https://mariusschulz.com/blog/literal-type-widening-in-typescript#widening-literal-types) to string:

```ts
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:

```ts
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:

```ts
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:

```ts
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!

```ts
const MODE = {
  CREATE: "CREATE",
  EDIT: "EDIT"
} as const;
```

It's equivalent to:

```ts
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!

```ts
type Mode = keyof typeof MODE;
```

Let's break it down. At type-level, the [`typeof` type operator](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html#the-typeof-type-operator) returns the type of a variable or property.

```ts
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](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html), on the other hand, expects an object type and returns a string or numeric literal union of its keys:

```ts
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.

```ts
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`](https://www.typescriptlang.org/docs/handbook/type-compatibility.html#enums) 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.