Borislav Hadzhiev
Fri Mar 11 2022·3 min read
Photo by twentyonekoalas
Use a user-defined type guard to check if an object implements an interface in TypeScript. The user-defined type guard consists of a function, which checks if the passed in object contains specific properties and returns a type predicate.
interface Employee { id: number; name: string; salary: number; } function isAnEmployee(obj: any): obj is Employee { return 'id' in obj && 'name' in obj && 'salary' in obj; } const emp: Employee = { id: 1, name: 'James', salary: 100, }; console.log(isAnEmployee(emp)); // 👉️ true console.log(isAnEmployee({ id: 1 })); // 👉️ false if (isAnEmployee(emp)) { // 👉️ TypeScript knows that emp is type Employee console.log(emp.id); // 👉️ 1 console.log(emp.name); // 👉️ "James" console.log(emp.salary); // 👉️ 100 }
We used a user-defined type guard to check if an object implements an interface.
obj is Employee
syntax is a type predicate where obj
must be the name of the parameter the function takes.If the isAnEmployee
function returns true
, TypeScript knows that the passed
in value is of type Employee
and allows us to access all properties and
methods on the specific interface.
The example above simply checks if the passed in object contains the id
,
name
and salary
properties.
This can get pretty verbose if your interface has many properties.
An alternative approach is to add a type
property to the interface, for which
you check instead.
interface Employee { id: number; name: string; salary: number; type: 'Employee'; // 👈️ add type property } function isAnEmployee(obj: any): obj is Employee { // 👇️ check for type property return 'type' in obj && obj.type === 'Employee'; } const emp: Employee = { id: 1, name: 'James', salary: 100, type: 'Employee', }; console.log(isAnEmployee(emp)); // 👉️ true console.log(isAnEmployee({ id: 1 })); // 👉️ false if (isAnEmployee(emp)) { console.log(emp.id); // 👉️ 1 console.log(emp.name); // 👉️ "James" console.log(emp.salary); // 👉️ 100 console.log(emp.type); // 👉️ "Employee" }
The Employee
interface has a type
property with the value of Employee
.
This means that all objects that have a type Employee
will have this property.
type
property that's equal to Employee
.However, note that this can get difficult to manage if you have interfaces that
extend from Employee
.
interface Employee { id: number; name: string; salary: number; type: 'Employee'; // 👈️ add type property } // ⛔️ Error: Interface 'Accountant' incorrectly // extends interface 'Employee'. // Types of property 'type' are incompatible. interface Accountant extends Employee { type: 'Accountant'; }
You can't simply override the type
property in the Accountant
interface.
If you have to do this, you'd get to set the type
property to be a string
,
but this would be difficult to manage if you have deeply nested structures.
User-defined type guard are very useful, especially when you have to check if an object is one of multiple types you know about in advance.
interface Dog { bark(): void; } interface Cat { meow(): void; } const dog: Dog = { bark() { console.log('woof'); }, }; const cat: Cat = { meow() { console.log('meow'); }, }; function isDog(pet: Dog | Cat): pet is Dog { return 'bark' in pet; } function getPet(): Dog | Cat { return Math.random() > 0.5 ? dog : cat; } const pet = getPet(); if (isDog(pet)) { console.log(pet.bark()); } else { // 👉️ TypeScript knows pet is Cat console.log(pet.meow()); }
The isDog()
function in the example takes a parameter of type Dog
or Cat
and checks if the passed in parameter is a Dog
.
Notice that in the if
block we are able to access dog-specific properties and
in the else
block, TypeScript knows that if pet
isn't a Dog
, then it will
be of type Cat
.