Do you really know TypeScript? (3): Types and interfaces

One of the things that you will do the most with Typescript is to define the shape of objects with type or interface. For that reason, understanding both well will make your TypeScript better quickly.

Let's see their main differences aside of the syntax, common patterns and surprising behaviours.

type can be used for more things

While interface is used just to define the shape of objects, type has other use cases.

type Pet = 'Cat' | 'Dog'

type CoolNumbers = 3.1416 | 4 | 100

Interface merging

You should be aware of this one.

interface DesktopFile {
  icon: string;
}

interface DesktopFile {
  extension: string;
}

// Error: Property 'icon' is missing in type '{ extension: string; }' 
// but required in type 'DesktopFile'.
const file: DesktopFile = {
  extension: 'pdf',
}

It can be surprising that you can redeclare an interface and merge them!

This is also known as "interface augmenting" and can be desirable in some situations but is definitely unusual in other languages.

Note that using Type would result in an error.

Discriminated union

Also known as "tagged union", is a frequent pattern in TypeScript.

It can be weird if you are used to polymorphism using classes, but since TypeScript's types disappear at runtime, you need to do things a bit different.

type File = {
    kind: 'file';

    name: string;
    extension: string;
}

type Folder = {
    kind: 'folder';

    name: string;
    filesInside: number;
}

type DesktopItem = File | Folder

const item: DesktopItem = {...}

if (item.kind === 'file'){
    // TypeScript knows that the properties
    // of the type File are defined here
}

This can be used like instanceof in other languages.

Union of types vs types of unions

Generally prefer union of types.

type Vehicle = {
    kind: 'motorcycle' | 'car'

    numberOfWheels: number 
    numberOfAirbags: number | undefined
}

const vehicle: Vehicle = {...}

if (vehicle.kind === 'car'){
    // TypeScript still thinks that
    // numberOfAirbags could be undefined
}

If we used union of types instead, like in the "discriminated union" example, TypeScript can be sure that the Car properties are available.

Excess property checking

This is a mechanism that can mess your mental model of structural typing when using type and interface.

interface Cat {
  name: string;
  whiskersLength: number;
}
const cat: Cat = {
  name: 'Uxia',
  whiskersLength: 6,
  bestFriend: 'Nina',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
//                    and 'bestFriend' does not exist in type 'Cat'
};

From a structural typing point of view it is valid as the defined object contains at least the properties declared for Cat.

This is excess property checking complaining though.

Check out this case:

type Person = {
    name: string;
    zipCode?: string;
}

const randomGuy: Person = {
    name: 'Pedro',
    zip: '45420',
}

Excess property checking quickly points out an error that we could've spent too much time looking for otherwise.

Note that this check only happens when using object literals.

Should I use type or interface then?

I find type easier to reason about and more readable.

One exception would be when extending types:

type Flyable = {
  fly(): void;
}

type Airplane = Flyable & {
  ...
}

interface Helicopter extends Flyable {
  ...
}

Also, as we saw earlier, "interface augmenting" can be unfamiliar to many people.

Be aware of their differences, try to make your team agree in their uses for the sake of consistency and you will be fine.

Resources to go deeper

23