The power of const assertions
Tags: typescript, webdev, programming
Published: 2021-01-04 11:32:25

[const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) were introduced in TypeScript 3.4 (March 2019), so they are not exactly new, yet I have seen that many developers are still unaware of that feature.

Maybe it's the syntax that makes it a bit weird sometimes (writing `const something = ... as const`). It might also be it's the resemblance to type casts that make people afraid of using them. Or possibly, you just got some weird errors regarding _readonly_, so you decided to not dig deeper.

In this article, I'd like to clear up the confusion and crush all doubts about const assertions.

### const assertions are _not_ type casts

Type casts are, simply put, evil. They are meant to tell the compiler: "I know what I am doing, and I know it better than you".

Frankly, most of the time, developers _do not_ know better than the compiler. So unless there is a really good reason, do not use type casts.

Here are some examples of what type casts allow you to do:

```ts
type Foo = 'foo'
const foo = 'bar' as Foo

type Obj = { foo: Foo }
const obj = {} as Obj
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nKBeKByAZg1BYAUAYzgDsBnYKTRFVAIwEMAnVKOk2BPPUSKAeRoBWyKAG8KCAFztEAXzyFS5OIOEiZLNvwF4gA)

TypeScript is fine with that, because the types sufficiently overlap (string with 'foo' and object with Obj). Of course, that is just false, but by using type casts, the compiler will yield to you.

This can be troublesome, even in cases where you think you are right. Consider the following example:

```ts
type Variant = 'primary' | 'secondary'

type Props = {
    variant: Variant
}

const Component = (props: Props) => null

const props = { variant: 'primary' }

Component(props)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWACgPRIoAFBOmEpYoAbw5RJUAG6IU6AFyw5aYBwC+HDvVSUMAYToEwDCOhEAKfIMpL+NgJRYAfDLpJGUAAxb2OvVDWQiKiMiqKuPhIRAgkUJqc7IbGpuhWAkIOQA)

Here, the compiler will complain with:

```
Argument of type '{ variant: string; }' is not assignable to parameter of type 'Props'.
  Types of property 'variant' are incompatible.
    Type 'string' is not assignable to type 'Variant'.(2345)
```

because variant will be inferred to string. TypeScript is doing this because nothing stops you from re-assigning another string to variant:

```ts
type Variant = 'primary' | 'secondary'

type Props = {
    variant: Variant
}

const Component = (props: Props) => null

const props = { variant: 'primary' }
props.variant = 'somethingElse'

Component(props)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWACgPRIoAFBOmEpYoAbw5RJUAG6IU6AFyw5aYBwC+HDvVSUMAYToEwDCOhEAKfIMpL+NgJRYAfDLpJGUAAxb2OvVDWQiKiMiqKuPhIRAgkUJrsQZQAdLLIqiI4lEYQwAAWSKgA5gCiADbUbJzshsam6FYCQg5AA)

Even though we define a _const_, objects in JavaScript are still mutable, so inferring the string literal 'primary' would be wrong. A type cast would solve this:

```ts
type Variant = 'primary' | 'secondary'

type Props = {
    variant: Variant
}

const Component = (props: Props) => null

const props = { variant: 'primary' } as Props

Component(props)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWACgPRIoAFBOmEpYoAbw5RJUAG6IU6AFyw5aYBwC+HDvVSUMAYToEwDCOhEAKfIMpL+NgJRYAfDLpJGUAAxb2OvVDWQiKiMiqKuPhIRAgkUOpQcML2Qr6GxqboVgJCDkA)

All good - except, it isn't. For the same reasons I mentioned earlier, if we remove primary from our _Variant_ type, we will not get a type error here. This means that, like many solutions, this is something that works now, but is not very future proof.

Making your software resilient to change is, in my opinion, one of the true benefits of using TypeScript. Achieving resilience requires the right mindset, which includes abandoning type casts.

For this scenario, the easiest solution (assuming that inlining the object is not an option) would be to use an explicit type annotation rather than a type cast:

```ts
type Variant = 'primary' | 'secondary'

type Props = {
    variant: Variant
}

const Component = (props: Props) => null

const props: Props = { variant: 'primary' }

Component(props)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWACgPRIoAFBOmEpYoAbw5RJUAG6IU6AFyw5aYBwC+HDvVSUMAYToEwDCOhEAKfIMpL+NgJRYAfDLpJGUAAxb2OvVDWQnYCQiKiMiqKuPhIRAgkUJqc7IbGpuhWoZQOQA)

This is likely what most of you are doing right now, and it is perfectly fine regarding type-safety.

### Using const assertions

I still think that fixing the issue with const assertions is the preferred way of doing it:

```ts
type Variant = 'primary' | 'secondary'

type Props = {
    variant: Variant
}

const Component = (props: Props) => null

const props = { variant: 'primary' } as const

Component(props)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWACgPRIoAFBOmEpYoAbw5RJUAG6IU6AFyw5aYBwC+HDvVSUMAYToEwDCOhEAKfIMpL+NgJRYAfDLpJGUAAxb2OvVDWQiKiMiqKuPhIRAgkUOpQcML+apzshsam6FYCQg5AA)

This comes in handy if you don't have the type available for annotation, for example, because it has not been exported from a library you are using. The syntax is also more terse, and using const assertions has other benefits as well. Because you are signalling TypeScript that your object is _really_ constant, the compiler can make better assumptions about your intentions:

-   strings and numbers can be inferred as their literal counterparts
-   arrays become tuples with a fixed length
-   everything is readonly, so you cannot accidentally mutate it afterwards (looking at you, _Array.sort_)

This will give you a ton of flexibility when working with that constant on type level.

### Extracting Types from Objects or Arrays

Consider the following example:

```ts
type Variant = 'primary' | 'secondary'
type Option = { id: Variant; label: string }

const options: Array<Option> = [
    {
        id: 'primary',
        label: 'The primary option',
    },
    {
        id: 'secondary',
        label: 'The secondary option',
    },
]
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAaghgJwJZwHbCgXigcjMgW0RBygB9cBnCAYwHtUATYnAWAChRIoB5MYJAyxQA3lCSMAXLEQp0AbigAbOACMIS6ZWDJUAcygBfDh3qptUOv0HnpAQQQI4IADx8BDAHzCA2hygBov6BIRLSeIQsADTBIQEq6pq4ACoAFtD4SEQIIJbWDDgx7CGGRSEisXFhVLQMzDmFlSEJGuFp0NR1xHkeqI3FgaUcALpAA)

So far, so easy. If we want to add another variant, we just have to add it to the Variant type _and_ to the options Array. This is fine as long as the code is co-located, and because we have explicitly annotated the options Array, but it can also become quite boilerplate-y pretty fast. With const assertions, you can just grab the type from the options Array:

```ts
const options = [
    {
        id: 'primary',
        label: 'The primary option',
    },
    {
        id: 'secondary',
        label: 'The secondary option',
    },
] as const

type Variant = typeof options[number]['id']
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBCAOUCW4IwLwwNoFgBQMhMA3vkeTEgCYBcMA5PAE5IC2AhkwJ70A0ZFQgBt2AIwCmQuvQAqAC3ExmbTlziIUYPgMIBffgSKlDg6tIjjQYKqu0mKIiVIbzFF8De7rk4O+X34ALow7GhW0Pj4UFzwigBqnEjsYLCY0bEgAGbemhBYYACurBJMgVj01PSBQA)

We are basically telling the compiler: Walk through every item of options and give me the type of the id. Much terser syntax, the type will still be correctly inferred to `primary | secondary`, and we now have one single source of truth.

Of course, this only works because of the const assertion, and if you remove it or forget it, _Variant_ will just be of type number. This is a problem because it relies on developers not making mistakes, and if we wanted to rely on that, we could also just write JavaScript.

Luckily, we can also tell the compiler to only make this work with readonly Arrays:

```ts
const options = [
    {
        id: 'primary',
        label: 'The primary option',
    },
    {
        id: 'secondary',
        label: 'The secondary option',
    },
] as const

type EnsureReadonlyArray<T> = T extends Array<any>
    ? never
    : T extends ReadonlyArray<any>
    ? T
    : never
export type Extract<
    T extends ReadonlyArray<any>,
    K extends keyof T[number]
> = EnsureReadonlyArray<T>[number][K]

type Variant = Extract<typeof options, 'id'>
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBCAOUCW4IwLwwNoFgBQMhMA3vkeTEgCYBcMA5PAE5IC2AhkwJ70A0ZFQgBt2AIwCmQuvQAqAC3ExmbTlziIUYPgMIBffgSKlDg6tIjjQYKqu0mKIiVIbzFF8De7rk4O+X34ALow7GhW0Pj4UFzwigCikACuTOIASuLsVOBCXACCTEzsXAA8MgB8GDAyMOIAHlDi1mj5hSXsYFxlOjAA-DBg4gBu4kzddNV1DU0w6ZnZeQVFxe2d3X0yY-1DI-h18CBMsNGxMHH1hcBQxd0T9Y1UaLNZYDktSytlBuQA0jV30wBrcRcEAAMyqWDAiVYEiYgXwFUwCQgyTSGWer0WJXKkOhsMCWG+8LwURiigAapwkO1YEjzuxLsVjuIwd5NBBeAxqPQykA)

This is admittedly a bit hackish, as it relies on the fact that const assertions make everything readonly. But it will make sure that Variant is inferred to _never_ if you forget the const assertion, and that means you won't be able to use it anywhere. I'll take that safety any day (and tuck _Extract_ away in a util).

### Use readonly everywhere

Finally, I'd like to point out something that became more apparent to me since I am constantly using const assertions:

> Make everything readonly per default

— TkDodo

The thing is: You _can_ pass a mutable Array or Object into a method that takes a readonly Array or Object, but _not_ the other way around. The reason is quite simple: if my function accepts an _Array_, the function might mutate it, so you cannot pass a _ReadonlyArray_ to it.

```ts
const getFirst = (param: Array<string>): string => param.sort()[0]

const strings = ['foo', 'bar', 'baz'] as const

getFirst(strings)
```

[TypeScript playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBA5gUygMQJYCdowLwwBQAOAhukQLYBcMAguqQJ4A806qYcAfAJRUttw4OMYqTIA6CCHRQ8XANoAGALoBYAFDrQkWH3YQcMOQHIAZiBBGANDCMAjElZv2AXkaUwi+rdHXrEKDGg8XTgILiA)

Even if you wouldn't sort the Array, it would still error with:

```
Argument of type 'readonly ["foo", "bar", "baz"]' is not assignable to parameter of type 'string[]'.
  The type 'readonly ["foo", "bar", "baz"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.(2345)
```

By making the parameter readonly, we just guarantee that we are not mutating it (which is always good - don't mess with function parameters) and we make people's lives easier if they are using const assertions in the process.

If you are a library author, I strongly recommend making all inputs to your functions readonly.

---

Do you also prefer const assertions? Leave a comment below ⬇️