crypto-community-spends-1-million-to-revive-elon-musk-inspired-token-floki-inu.jpg

Typescript type narrowing

Source Node: 1857482

Many times we need to be able to differentiate between Typescript interfaces which have some properties in common. Take the following example:

interface Vehicle { weight: number; wheels?: number; class?: 'A' | '1' | '2' | '3';
}

In our application we have different vehicles. For trucks, for example, we want to keep track of their weight, and their number of wheels. However other vehicles (like yatches) have no wheels, but have other attributes we want to know, like for instance their class, which is a string that can take values ‘A’, ‘1’, ‘2’, or ‘3’.

We might think that a good configuration can be as follows:

interface Truck { weight: number; wheels: number;
} interface Yatch { weight: number; class: 'A' | '1' | '2' | '3';
} export type Vehicle = Truck | Yatch;

However, with this setup we will have a problem when trying to use Vehicle objects:

As we can see, Typescript can’t decide whether the input object is a Truck or a Yatch, and it throws the following error:

Property 'wheels' does not exist on type 'Yatch'

How can we narrow the object’s type? There are at least 3 alternatives:

1. Create a new type property on each of the interfaces

interface Truck { weight: number; wheels: number; type: 'truck';
} interface Yatch { weight: number; class: 'A' | '1' | '2' | '3'; type: 'yatch';
} export type Vehicle = Truck | Yatch;

This is known as discriminated unions in Typescript. This solution would be the most adequate if our interfaces already had the type attribute beforehand.

When using Vehicle objects, now we can use a switch case to differentiate both interfaces:

If we are using classes instead of interfaces for our types, the following alternative syntax produces the exact same result:

class Truck { weight: number; wheels: number; readonly type = 'truck';
} class Yatch { weight: number; class: 'A' | '1' | '2' | '3'; readonly type = 'yatch';
} export type Vehicle = Truck | Yatch;

2. Use type guards

Type guards are functions that help with type narrowing.

interface Truck { weight: number; wheels: number;
} interface Yatch { weight: number; class: 'A' | '1' | '2' | '3';
} export type Vehicle = Truck | Yatch; export function isTruck(arg: any): arg is Truck { return !!arg.weight && !!arg.wheels;
} export function isYatch(arg: any): arg is Yatch { return !!arg.weight && !!arg.class;
}

If isTruck(object) returns true, it means that our object is indeed a Truck. We can import and use these functions anywhere in our application:

However, there is a small problem with this setup: we can still build Vehicle objects which are not Truck nor Yatch:

3. Use “never”

In order to fix this last problem we can use the never type, introduced in Typescript 2.0. If an interface has a property of type never, then that property can not be defined on any object which follows that interface.

interface Truck { weight: number; wheels: number; class?: never;
} interface Yatch { weight: number; wheels?: never; class: 'A' | '1' | '2' | '3';
} export type Vehicle = Truck | Yatch;

Type guards work exactly as they previously did, but now we can’t create Vehicle objects which have at the same time both the wheels and class properties:

As we can see we obtain the following error:

Type 'number' is not assignable to type 'never'

Check the Spanish version of this post at my blog cibetrucos.com

Source: https://www.codementor.io/davidsilvasanmartin/typescript-discriminated-unions-1jhqp24mvx

Time Stamp:

More from Codementor Community