Narrow interfaces in TypeScript
In this post, I’m going to discuss how to use TypeScript’s type guards to narrow interfaces. If you have an union type as method parameter, for instance A | B
where A
and B
are interfaces, you probably want to avoid ugly type assertions (type casting) within the method to access specific interface properties. Type assertions could look as follows:
What is the best way to write the code without type assertions? Let’s design a concret model. The model consists of the interface Animal
, two extended interfaces WildAnimal
, Pet
, and two classes Dog
and Cat
which implement the interface Pet
.
Now, I would like to write a function which accepts both interfaces WildAnimal
and Pet
. This function should print the common property kind
and the specific properties for each interface. If we have Pet
, the property name
should be printed. Otherwise, the property livingArea
. How to check the given interface? One possibility is so called user-defined type guards. A such type guard is a function with the return type as type predicate. Let’s name this function isPet
.
TypeScript will narrow the given parameter animal
to that specific type within if
and else
blocks. No type casting is needed. The output for classes Dog
and Cat
looks like as follows:
This is fine, but I personally don’t like to write an extra function as type guard. It’s matter of taste, but the if-check doesn’t look good for me. What is about a simpler check with instanceof
type guard? The instanceof
operator can be used in TypeScript as a type guard too. It allows to narrow types, but not for interfaces! If I would write animal instanceof Pet
, the TypeScript playground would show an error — cannot find name ‘Pet’.
The error message is correct. The name Pet is not defined in the variable declaration space. In other words, interfaces don’t exist in the transpiled code. Look at the ES5 code TypeScript produces.
Fortunately, TypeScript 2.0 has introduced a new feature called discriminated unions. We can define a common property in every interface which can be checked in the switch — case
statement. The common property is called discriminant. The switch — case
statement acts as type guard and make the type casting unnecessary.
A new model has now enum AnimalKind
whose values are used for the discriminant (enums are less error-prone than hard-coded strings). Both interfaces WildAnimal
and Pet
get the discriminant property kind
and a new property subkind
. The interface Animal
is gone.
As you can see, the classes Dog
and Cat
get the discriminant property kind
which has to be set in the constructor. This property can only accept the enum value AnimalKind.PET
because Dog
and Cat
implement the interface Pet
.
The updated function printAnimal
examines the property kind
and controls the program flow via the mentionedswitch — case
statement.
The output is the same as before.