Do you really know TypeScript? (4): Type assertions

In this post (the last of the series 😢) we are going to understand type assertions and compare them against type declarations.

What do you use type assertions for?

There are situations where you know more than TypeScript can infer.

let item: Item = {...}

type Item = {
  name: string
}

type FileItem =  Item & {
  extension: string
}

// We know for sure that item
// is also a file
printFile(item as File)

Golden rule for using assertions

You can only assert from one type to another if either type is a subset of the other. 🧐

type Car = {
  numOfDoors: number
}

type Airplane = {
  numOfEngines: number
}

const car: Car = {numOfDoors: 5}

// Conversion of type 'Car' to type 'Airplane' may be a mistake
// because neither type sufficiently overlaps with the other. 
const airplane = car as Airplane

An exception to this rule is when using unknown or any.
You can use these to bypass it:

  • unknown because is the universal set
  • any because disables the type checking
const airplane = car as unknown as Airplane

Prefer type declarations to type assertions

This is a mistake that I've seen a lot!

type Car = {
  numOfDoors: number
  numOfAirbags: number
}

// Error: Property 'numOfAirbags' is missing
const car: Car = {numOfDoors: 5}

// No error
const car = {numOfDoors: 5} as Car

When you use type assertions you are telling TypeScript to get out of the way, with type declarations you are making your intentions clear so it can help you.

Is as const a type assertion?

It is not.
Despite having a similar syntax, as const is used to hint the type system about values being immutable.

It is very situational, but could be useful for using the values of an array as literals, for example:

const coolBands = ['Oasis', 'AC/DC', 'Foo Fighters'] as const

// type CoolBands = "Oasis" | "AC/DC" | "Foo Fighters"
type CoolBands = typeof coolBands[number]

Or for using the values of an object:

const coolBandsAndSingers = {
  'Oasis': 'Liam Gallagher',
  'AC/DC': 'Brian Johnson',
  'Foo Fighters': 'Dave Grohl'
} as const

// type CoolBands = "Oasis" | "AC/DC" | "Foo Fighters"
type CoolBands = keyof typeof coolBandsAndSingers

// type CoolSingers = "Liam Gallagher" | "Brian Johnson" | "Dave Grohl"
type CoolSingers = typeof coolBandsAndSingers[CoolBands]

As it is the last post of this series, I also want to go through some topics that couldn't get a post for their own.

Don't type everything!

I did it, and probably so did you.

It is not bad, but can make the code too verbose and therefore harder to read.

As a rule of thumb, you should type very well:

  • Function and method signatures (parameters and return types)
  • Variables and constants when using object literals, to take advantage of excess property checking.

In a TDD like spirit, you should know your input and output types before implementing a function/method, so typing it from the beginning makes it easier for you to implement it.

Typing return types usually avoids implementation errors, specially if your function has many "paths".

Don’t use uppercase variants of primitive types

Probably you noticed that String or Number exist and wonder if you should use them as types.

The answer is no. Just stick to lowercase types for primitives string, number, boolean, etc.

These uppercase variants exist primarily for convenience, for example:

// charAt is not a property of
// the string primitive
"hey".charAt(1)

JavaScript wraps the string primitive in String under the hood and uses the charAt method of String and then throws that object away.

// These wrappers don't have behave 
// as primitives

new String('hey') === new String('hey')

'hey' === new String('hey')

It's been a pleasure to write this series and I wish you a very productive experience with TypeScript 🙂

Thanks for reading!

Resources to go deeper

26