TypeScript Type Guards |
Type Guards are an important type narrowing mechanism in TypeScript.
They allow developers to accurately infer variable types at runtime through specific conditional checks.
With type guards, we can safely access properties and methods of specific types within union type variables.
Why Type Guards Are Needed
In TypeScript, a variable may be declared as a union of multiple types.
When we need to perform different operations based on different types, the compiler cannot automatically determine the current specific type.
Type Guards are the key mechanism to solve this problem.
Concept Explanation: The core principle of Type Guards is "Type Narrowing". Through conditional judgments, TypeScript will automatically narrow down the union type to a specific type.
typeof Type Guard
typeof is the most commonly used type guard, used to check primitive types (string, number, boolean, etc.).
It returns a string representing the type of the value.
Basic Usage of typeof
// Define a function that accepts a union type
// Parameter value could be string or number
function printValue(value: string | number):void{
// Use typeof to check type
// When the condition in if is true, TypeScript will automatically narrow value to string type
if(typeof value ==="string"){
// At this point, TypeScript knows value is string
// Can safely access the length property of string
console.log("String length: "+ value.length);
}else{
// When entering else branch, TypeScript knows value is not string
// It can only be number type
// Can safely perform mathematical operations
console.log("Double the number: "+(value *2));
}
}
// Test call
printValue("hello");// Pass in string
printValue(42);// Pass in number
Output:
String length: 5
Double the number: 84
Types Supported by typeof:
- "string" - String type
- "number" - Number type (including NaN and Infinity)
- "boolean" - Boolean type
- "undefined" - Undefined type
- "object" - Object type (Note: Arrays and null will also be identified as "object")
- "function" - Function type
Note:
typeofreturns "object" for both arrays and null. If you need to distinguish between arrays and objects precisely, other methods are required.
instanceof Type Guard
instanceof is used to check whether an object is an instance of a certain class.
It determines the type by checking the object's prototype chain.
Basic Usage of instanceof
// Define Dog class
class Dog {
// Method for dog's bark
bark():void{
console.log("Woof woof!");
}
}
// Define Cat class
class Cat {
// Method for cat's meow
meow():void{
console.log("Meow meow!");
}
}
// Function accepting union type
function makeSound(animal: Dog | Cat):void{
// Use instanceof to check if animal is Dog or Cat
// When the condition in if is true, TypeScript narrows animal to Dog type
if(animal instanceof Dog){
// Now we can call Dog-specific methods
animal.bark();
}else{
// In else branch, TypeScript narrows animal to Cat type
animal.meow();
}
}
// Test call
makeSound(new Dog());// Create Dog instance and call
makeSound(new Cat());// Create Cat instance and call
Output:
Woof woof!
Meow meow!
Explanation:
instanceof checks the object's prototype chain, so it can only be used with class instances, not interfaces or type aliases.
Custom Type Guards
When built-in typeof and instanceof do not meet requirements, custom type guard functions can be created.
Custom type guards use the return type syntax value is Type.
Custom Guard Functions
// Define a custom type guard function
// Return type uses "value is Type" format
// This tells TypeScript: when the function returns true, the parameter type is string
function isString(value: any): value is string {
// Use typeof to check if it's a string
return typeof value ==="string";
}
// Another custom guard: check if it's a number
function isNumber(value: any): value is number {
return typeof value ==="number";
}
// Define an array type guard
function isArray(value: any): value is any[]{
return Array.isArray(value);
}
// Function to process values
function processValue(value: string | number | any[]):void{
// Use custom guards for type checking
if(isString(value)){
// TypeScript knows value is string type
// Can call toUpperCase() method
console.log("String to uppercase: "+ value.toUpperCase());
}else if(isNumber(value)){
// TypeScript knows value is number type
// Can call toFixed() method
console.log("Number formatted: "+ value.toFixed(2));
}else if(isArray(value)){
// TypeScript knows value is array type
console.log("Array length: "+ value.length);
}
}
// Test calls
processValue("hello");
processValue(3.14159);
processValue([1,2,3,4,5]);
Output:
String to uppercase: HELLO
Number formatted: 3.14
Array length: 5
Tip: The key to custom type guards is the return type
value is Type, which is the identifier that TypeScript uses to recognize type guards.
in Operator Type Guard
The in operator can check if an object contains a specific property.
When used in conditional statements, TypeScript will automatically narrow the object's type range.
Usage of in Operator
// Define two interfaces with different properties
interface A {
a: string;// Only property a
}
interface B {
b: number;// Only property b
}
// Function accepting union type
function process(obj: A | B):void{
// Use in to check if object has property "a"
if("a"in obj){
// In if branch, TypeScript knows obj contains property a
// So obj's type is narrowed to A
console.log("Property a of A: "+ obj.a);
}else{
// In else branch, obj does not contain property a
// TypeScript knows obj must be type B
// So we can safely access property b
console.log("Property b of B: "+ obj.b);
}
}
// Test calls
process({ a:"hello"});// Pass in object with property a
process({ b:42});// Pass in object with property b
Output:
Property a of A: hello
Property b of B: 42
Discriminated Unions and Type Guards
A discriminated union is a powerful pattern that distinguishes members of a union type through a common "identifier" property.
Combined with switch statements or if checks, it enables complete type guarding.
Discriminated Union Implementation - Calculator
// Define Circle interface, using kind property as identifier
interface Circle {
kind:"circle";// Identifier field: value is "circle"
radius: number;// Radius
}
// Define Rectangle interface
interface Rectangle {
kind:"rectangle";// Identifier field: value is "rectangle"
width: number;// Width
height: number;// Height
}
// Define Triangle interface
interface Triangle {
kind:"triangle";// Identifier field: value is "triangle"
base: number;// Base
height: number;// Height
}
// Define union type
type Shape = Circle | Rectangle | Triangle;
// Function to calculate area
function getArea(shape: Shape): number {
// Use switch statement for type guarding
// Based on the value of kind property, TypeScript will automatically narrow the type
switch(shape.kind){
case"circle":
// shape is narrowed to Circle type
// Can access radius property
return Math.PI* shape.radius**2;
case"rectangle":
// shape is narrowed to Rectangle type
// Can access width and height properties
return shape.width* shape.height;
case"triangle":
// shape is narrowed to Triangle type
return 0.5* shape.base* shape.height;
}
}
// Test calls
var circle ={ kind:"circle" as const, radius:5};
var rectangle ={ kind:"rectangle" as const, width:4, height:6};
var triangle ={ kind:"triangle" as const, base:3, height:4};
console.log("Circle area: "+ getArea(circle).toFixed(2));
console.log("Rectangle area: "+ getArea(rectangle));
console.log("Triangle area: "+ getArea(triangle));
Output:
Circle area: 78.54
Rectangle area: 24
Triangle area: 6
Explanation: Discriminated unions are one of the most recommended patterns in TypeScript. They distinguish different type members through a common literal property (usually kind or type), making the code both type-safe and maintainable.
Null and Undefined Checks
When handling values that might be null or undefined, direct equality checks are also effective type guards.
Null Check
// Define a function parameter that might be null
function getLength(str: string |null): number {
// Directly check if str is not null
// When condition is true, TypeScript knows str is not null
// At this point, we can safely access str's properties
if(str !==null){
return str.length;
}
// Handle null case
return 0;
}
// Test calls
console.log(getLength("hello"));// Normal string
console.log(getLength(null));// Pass in null
Output:
5
0
Tip: After enabling strictNullChecks, it is recommended to always perform null checks. Optional chaining (?.) and nullish coalescing (??) can simplify code.
Truthiness Narrowing
Besides explicit type checks, TypeScript also narrows types through truthiness assertions.
Truthiness Narrowing
// String that might be undefined
function greet(name?: string): string {
// Use short-circuit operator: if name is undefined or empty string, use default value
// In the code block after &&, TypeScript knows name definitely has a value
return name &&"Hello, "+ name;
}
// Test
console.log(greet("TUTORIAL"));
console.log(greet());
Output:
Hello, TUTORIAL
Hello, undefined
Important Notes
- Type guards must be used in conditional branches: Only after using type guards in conditional judgments will TypeScript perform type narrowing
- Return type must be a type predicate: The return type of custom type guards must be in the format
value is Type - Discriminated unions are best practices: For complex union types, it is recommended to use the discriminated union pattern
- Be mindful of completeness of type narrowing: When using switch statements, it is recommended to handle all possible branches
Suggestion: When dealing with union types, prioritize using the discriminated union pattern. It not only makes the code clearer but also fully leverages TypeScript's type inference capabilities.
Summary
Type guards are an essential part of TypeScript's type system.
- typeof: Most commonly used, suitable for checking primitive types
- instanceof: Checks if an object is an instance of a specific class
- Custom guards: Achieve flexible class checks using the
value is Typesyntax - in: Checks if an object contains a specific property
- Discriminated unions: Recommended best practice pattern, distinguishing types via identifier fields
- Truthiness narrowing: Uses JavaScript's truthiness judgment to narrow types
YouTip