What are 'const assertions' in TypeScript? The famous 'as const'

technology3 min

byLucas Santos

This page was machine translated. Read original / Suggest a fix

This article complements my video on the same topic! Watch it here:

Play

If you’re coding in TypeScript, you’ve probably come across a very strange type, the famous const as const, something like this:

const foo = {
bar: 'string'
} as const

In fact, we even talked about it when we discussed enums here on the blog. You might think that because it has an as, it must be some kind of type casting, that is, we’re swapping one type for another and forcing TS to accept it, which is bad practice. But it’s not!

This kind of technique is called const assertions and, as the name says, it’s an assertion, that is, we’re giving more information about a type to TypeScript. But what are we telling it?

Objects and arrays#

This kind of assertion is generally used with objects and arrays, and it’s also the simplest way to understand the concept. But imagine you have this object:

const args = [1, 2]
const sum = (a, b) => a+b

All TS knows is that args is an array of numbers so it will type it as number[], which is acceptable because you could, for example, do an array.push(0) and it would accept it without problems, actually, in most cases that’s what happens. But there’s one case where it doesn’t.

There are several functions in JavaScript that require an exact number of arguments, an example is the Math.atan2 function which takes exactly 2 parameters. Another example is our sum function. So this code doesn’t work:

const args = [1, 2]
const sum = (a, b) => a+b
sum(...args)

Because TS can’t infer how many parameters args has, or the total number of items in the array, precisely because we can add, remove, or modify the array however we want.

If we want to say that this array is immutable, a constant, we have to use a const assertion

const args = [1, 2] as const
const sum = (a, b) => a+b
sum(...args)

In this case, args is now typed as readonly [1, 2], that is, we can’t modify the array, and it has exactly two elements, 1 and 2. Therefore we can pass it to the function because we tell TS that it will always have two elements.

The same goes for objects, if we pass any object like this:

const obj = {
foo: 1,
bar: 'formacaots.com.br'
}

TypeScript will simply type it as an object like this:

const obj: {
foo: number;
bar: string;
}

And we can add or remove keys, in addition to being able to pass any string and any number, or even change the type of the object, but if we use

const obj = {
foo: 1,
bar: 'formacaots.com.br'
} as const

Our typing changes to:

const obj: {
readonly foo: 1;
readonly bar: "formacaots.com.br";
}

See that now it’s not just immutable, but also has literal types as keys. And you might be wondering: “What about Object.freeze? Doesn’t it do the same thing? Isn’t it even safer because it’s at runtime?”

There’s a fundamental difference between Object.freeze and as const, while Object.freeze will indeed make the object immutable at runtime, it only does this for the first level of keys, so nested and composite keys like:

const foo = {
bar: {
baz: 1
}
}

Don’t work, if you use Object.freeze(foo), that ensures that bar can’t be changed to another object, but foo.bar.baz can be changed normally. With as const, the entire object would be immutable.

Conclusion#

const assertions do three things:

  • With primitive types (string, number, boolean, etc), it removes the possibility of type widening, that is, a 'foo' becoming string.
  • Object properties become readonly
  • Arrays become immutable tuples (readonly)

You can even use as const in function returns to make the inferred type of the function always be inferred as the most static type possible:

function foo () {
return [1, 2] as const // retorna uma tupla
}