Understanding TypeScript type inference and const assertions

Adrien HARNAY
Brigad Engineering
Published in
4 min readDec 2, 2021

--

A plane dashboard with lots of buttons and levers, and a pilot and copilot
Photo by Franz Harvin Aceituna on Unsplash

TypeScript is “a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale”. By statically checking your code (during development), TypeScript shields you from runtime errors and is a great copilot for small to enormous projects.

That being said, when I introduce TypeScript to newer developers, there comes a moment when it slows them down instead of helping them. I believe this is due to the learning curve of TypeScript: it will first take you time to learn when to use which features of the language, and you might be tempted to type everything “just to be safe”, but this ends up taking you a lot of time for no real benefit.

How to get past this point? This is what I will try to cover in this article, focusing mainly on type inference and const assertions.

Type inference in TypeScript

Unlike some other typed languages with which we are required to type everything, TypeScript is able to understand most of the code we will write without explicitly writing types.

Consider the following code:

const implicit = 1; const explicit: number = 1;
TypeScript understands the `implicit` variable is a number

Here, TypeScript is able to infer the type of the implicit variable. This means we don’t need to specify it ourselves! Even more so, not explicitly specifying the type is better because TypeScript infers the type to be 1 instead of number, which is more precise and closer to reality.

This brings us to a very important rule I follow when writing code with TypeScript:

Only write types when TypeScript does not understand what we are doing.

Expanding on our previous example:

const implicit = 1; const explicit: number = 1; const acceptOnlyOne = (number: 1) => {}; acceptOnlyOne(implicit); acceptOnlyOne(explicit);
By explicitly setting the type to `number`, TypeScript doesn’t treat the value as valid for a function accepting `1`

The acceptOnlyOne function will only accept 1 as an argument. When defining the type as number for a variable, even if the value is in fact 1, the function will not accept it. As we said earlier, it is better to not type the variable and let TypeScript infer its type.

The same principle applies to function return types:

Typing the function return does not bring any value, and at worst prevents TypeScript from inferring a precise type.

Const assertions

TypeScript type inference works in the example above because we are defining a variable using the const keyword and assigning it a literal value. TypeScript understands the value is read-only. What would happen if we wanted to assign an object, an array, or the return value of a function?

const implicit = { number: 1 };
As objects are mutable in JavaScript, the type for the properties of the object is generic

As we can see, implicit.number is inferred as a number, not as 1.

If we need to pass it to a function that accepts only objects with certain values as properties:

const implicit = { number: 1 }; const acceptObjectWithOnlyOne = (obj: { number: 1 }) => {}; acceptObjectWithOnlyOne(implicit);
The function won’t accept the object because the type for the `number` property is too generic

We can see the function will not accept the parameter because the type has not been inferred as a literal value but as a generic type. We need to perform a const assertion.

const implicit = { number: 1 } as const;
By using `as const`, TypeScript properly infers the type for the properties of the object

By using as const on a variable, we hint TypeScript that it is read-only and we narrow the type to its literal value.

This brings us to a second rule I follow when writing code with TypeScript:

Always use const assertions when working with objects, arrays, and return values of functions if we need precise types.

const implicit = { number: 1 } as const; const acceptObjectWithOnlyOne = (obj: { number: 1 }) => {}; acceptObjectWithOnlyOne(implicit);
By using `as const`, the acceptObjectWithOnlyOne function accepts the object because it properly infers the type of its properties

It also works with arrays:

const numberArray = [1, 2, 3];
Declaring an array, the type will always be generic when not using `as const`
const numberArrayConst = [1, 2, 3] as const;
When using `as const`, the type of the array is narrowed to the literal value
let state = 0; const incrementState = () => { state = state += 1; } const mixedArray = [state, incrementState]; const firstElement = mixedArray[0];
Mimicking the return of React’s useState, we define an array of size 2 (or tuple) with a state and a setter. When accessing the first element, TypeScript does not correctly infer the type
let state = 0; const incrementState = () => { state = state += 1; } const mixedArray = [state, incrementState] as const; const firstElement = mixedArray[0];
When using `as const`, TypeScript understands the first element is a number

And for function returns:

const getMessage = () => ‘Hello!’; const message = getMessage();
The return value of a function is always inferred in a generic way when not using `as const`
const getMessage = () => ‘Hello!’ as const; const message = getMessage();
When using `as const`, the return type of the function is narrowed to the literal value

Wrapping up

There are a lot of advanced concepts that will make writing code with TypeScript a more pleasant experience, but focusing on type inference and const assertions at the beginning will save you a lot of time and will make TypeScript work for you instead of against you.

--

--