Type Guards in TypeScript
Type Guards are used to check whether you are dealing with a certain type or not. Getting from a more general type to a more specific type is called Narrowing. There are different ways to ‘type guard’, depending on what to test against. You can test against native values (string, number, boolean etc.), instances of classes or custom types.
Type Guard for native values
Here we check if year
is a string or a number using typeof
.
function showNextYear(year: string | number) { if(typeof year === "string") { const nextYear = parseInt(year, 10) + 1; console.log(`Next year is ${nextYear}`); } else { const nextYear = year + 1; console.log(`Next year is ${nextYear}`); } } showNextYear("2021"); showNextYear(2021);
Truthiness narrowing
Here we narrow down the type by checking the truthiness of strs
:
function printAll(strs: string | string[] | null) { if (strs && typeof strs === "object") { for (const s of strs) { console.log(s); } } else if (typeof strs === "string") { console.log(strs); } }
Instanceof Type Guard
class Deed { constructor(public action: string) { } } class Good extends Deed { constructor(action: string, public goodThing: string) { super(action); } } class Bad extends Deed { constructor(action: string, public badThing: string) { super(action); } } function yourAction(deed: Good | Bad) { if(deed instanceof Good) { console.log("Well done"); } else { console.log("Please stop"); } } yourAction({ action: "donate", goodThing: "money" }); yourAction({ action: "waste", badThing: "food" });
Custom Type Guard
interface Deed { action: string; } interface Good extends Deed { goodThing: string } interface Bad extends Deed { badThing: string } function isGood(deed: Deed): deed is Good { return deed.hasOwnProperty("goodThing"); } function yourAction(deed: Good | Bad) { if(isGood(deed)) { console.log("Well done"); } else { console.log("Please stop"); } } yourAction({ action: "donate", goodThing: "money" }); yourAction({ action: "waste", badThing: "food" });
More narrowing
TypeScript is rather smart about automatically narrowing down types. In the following example we check if x is the same as y. If so, they both must be strings:
function example(x: string | number, y: string | boolean) { if (x === y) { // We can now call any 'string' method on 'x' or 'y'. x.toUpperCase(); (method) String.toUpperCase(): string y.toLowerCase(); (method) String.toLowerCase(): string } else { console.log(x); (parameter) x: string | number console.log(y); (parameter) y: string | boolean } }
Same goes for in operator:
type Fish = { swim: () => void }; type Bird = { fly: () => void }; type Human = { swim?: () => void, fly?: () => void }; function move(animal: Fish | Bird | Human) { if ("swim" in animal) { animal (parameter) animal: Fish | Human } else { animal (parameter) animal: Bird | Human } }