Contents

TypeScript: Type Declarations vs Type Assertions

What are they about?

In TypeScript there are two ways of assigning a value to a variable and giving it a type:

  • Type declaration
  • Type assertion

We will talk about what they are and which one is better.

Example

Say we have an interface Person

1
2
3
interface Person {
    name: string;
}

We can give the Person type to a variable by declaring it:

1
const john: Person = { name: 'John' }

or by asserting it:

1
const bingo = { name: 'Bingo' } as Person

In the first example we declare that the type is Person with the statement :Person and we make sure that the value conforms to its type.

In the second example instead, we perform a type assertion with the statement as Person. In other words we are telling the type checker that, despite the type of bingo is inferred, we know better and would like the type to be Person.

The end results are similar but actually quite different.

Imagine the following scenarios:

Error
1
const john: Person = { }

Property name is missing in type {} but required in type Person

or

Error
1
const john: Person = { name: 'John', email: 'example@example.com' }

Type { name: string; email: string; } is not assignable to type Person.

On the other hand if we do the same but with type assertion we get different results:

Success
1
const bingo = { } as Person
1
const bingo = { name: 'Bingo', owner: 'John' } as Person

As you see, type assertion silences the type checker by asserting that we know better than it does.

How to deal with arrow functions

It is not always clear how to use type declarations with arrow functions.

For example, say we want to map an array of names into an array of Person.

The first thing we could try is something like the following:

1
const people = ['John', 'Lucy'].map(name => ({name}))

The problem is that people will be of type { name: string; }[] while we want Person[].

We might be tempted to assert the returned object:

1
const people = ['John', 'Lucy'].map(name => ({name} as Person))

Now people is of type Person[] as we wanted but all the cons we mentioned earlier.

The best way to deal with arrow function is declaring the return type:

A better way
1
const people = ['John', 'Lucy'].map((name): Person => ({name}))

So, which one should I use?

I hope it is now clear that you should always prefer type declarations because they offer additional safety checks, unless you have a good reason to think you know better than TypeScript.

Typically this happens when a type comes from a context that is unknown to the type checker, for instance the type of a DOM element retrieved via DOM API.

Consider the following example:

1
const button = document.getElementById('my-element')

TypeScript doesn’t have a clue of what type of element my-element could be or whether or not it is supposed to be found because it has no access to the DOM of the page, therefore it infers the type HTMLElement | null.

But because we do have access to the DOM we have more information and know that it is a button and it won’t be null so we can safely assert its type.

1
const button = document.getElementById('my-element') as HTMLButtonElement

The type of button is now HTMLButtonElement.

There is also the situation where you don’t know what kind of element my-element could be but you know it won’t be null, in these cases you can perform a non-null assertion by using the ! operator:

1
const element = document.getElementById('my-element')!

and element will have type HTMLElement.

Conclusion

Always prefer type declarations : Type over type assertions as Type or non-null assertions ! unless you are sure you know something about types that TypeScript does not.