- TypeScript Tutorial
- TypeScript Home
- TypeScript Introduction
- TypeScript Setup
- TypeScript First Program
- TypeScript vs JavaScript
- TypeScript Data Types
- TypeScript Type Inference
- TypeScript Type Annotations
- TypeScript Interfaces
- TypeScript Enums
- TypeScript Type Aliases
- TypeScript Type Assertions
- TypeScript Variables
- TypeScript Functions
- TypeScript Functions
- TypeScript Optional Parameters
- TypeScript Default Parameters
- TypeScript Rest Parameters
- TypeScript Arrow Functions
- Classes and Objects
- Introduction to Classes
- Properties and Methods
- Access Modifiers
- Static Members
- Inheritance
- Abstract Classes
- Interfaces vs Classes
- Advanced Types
- TypeScript Union Types
- TypeScript Intersection Types
- TypeScript Literal Types
- TypeScript Nullable Types
- TypeScript Type Guards
- TypeScript Discriminated Unions
- TypeScript Index Signatures
- TypeScript Generics
- Introduction to Generics
- TypeScript Generic Functions
- TypeScript Generic Classes
- TypeScript Generic Constraints
- TypeScript Modules
- Introduction to Modules
- TypeScript Import and Export
- TypeScript Default Exports
- TypeScript Namespace
- Decorators
- Introduction to Decorators
- TypeScript Class Decorators
- TypeScript Method Decorators
- TypeScript Property Decorators
- TypeScript Parameter Decorators
- Configuration
- TypeScript tsconfig.json File
- TypeScript Compiler Options
- TypeScript Strict Mode
- TypeScript Watch Mode
Type Guards in TypeScript
In TypeScript, you often deal with variables that could be one of several types this is known as a Union Type (e.g., string | number). However, if you try to access a property that only exists on a string when the variable might still be a number, TypeScript will throw an error to protect you.
Type guards are the solution to this problem. They are expressions that perform a runtime check to narrow down the type of a variable within a specific conditional block. Once a type guard is triggered, TypeScript "remembers" the result and allows you to safely access type-specific properties and methods inside that scope.
Key Concepts of Type Guards
- Type Guard: A logical check (usually inside an
iforswitchstatement) that tells the TypeScript compiler a variable is definitely of a specific type. - Custom Type Guards: Specialized functions that return a "type predicate," allowing you to encapsulate complex validation logic that can be reused across your project.
- Built-in Type Guards: Native JavaScript operators like
typeof,instanceof, andinthat TypeScript leverages to understand your code's intent automatically.
as keyword) when possible. Type guards provide runtime safety, whereas assertions just tell the compiler to "trust you," which can lead to hidden bugs if you're wrong.
Example Usage of Type Guards
Example 1: Using typeof for Primitive Types
The typeof operator is the simplest way to distinguish between basic JavaScript types like strings, numbers, and booleans. It is perfect for handling raw data from inputs or configuration files.
function formatValue(value: string | number): string {
if (typeof value === "string") {
// TypeScript knows 'value' is a string here
return value.trim().toUpperCase();
} else {
// TypeScript knows 'value' must be a number here
return value.toFixed(2);
}
}
console.log(formatValue(" hello ")); // Output: "HELLO"
console.log(formatValue(42.567)); // Output: "42.57"
In this example:
- The
typeofcheck identifies whethervalueis a primitivestring. - Inside the
ifblock, you get full autocompletion for string methods like.trim(). - Inside the
elseblock, TypeScript is smart enough to know that if it isn't a string, it must be the only other option: anumber.
typeof null. In JavaScript, typeof null returns "object". If your union type includes null, a simple typeof check might not be enough to distinguish it from an actual object or array.
Example 2: Using instanceof for Class Types
When working with Object-Oriented Programming (OOP) and custom classes, instanceof is your best friend. It checks if an object was constructed from a specific class.
class FileLogger {
logToFile(msg: string) { console.log(`Writing to file: ${msg}`); }
}
class ApiLogger {
sendToCloud(msg: string) { console.log(`Sending to API: ${msg}`); }
}
function executeLog(logger: FileLogger | ApiLogger, message: string): void {
if (logger instanceof FileLogger) {
logger.logToFile(message);
} else {
logger.sendToCloud(message);
}
}
Here:
instanceoflooks at the constructor of the object at runtime.- It allows you to safely call methods that exist on
FileLoggerbut not onApiLogger, and vice versa.
instanceof only works with classes. It will not work with TypeScript Interfaces because interfaces are removed (erased) during compilation and do not exist at runtime.
Example 3: Using in Operator for Object Types
The in operator is highly effective when you are dealing with different object shapes (interfaces) that don't use classes. It checks for the existence of a specific property name.
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
function printDetails(user: Admin | Employee) {
console.log(`User: ${user.name}`);
if ("privileges" in user) {
// Narrowed to Admin
console.log(`Privileges: ${user.privileges.join(", ")}`);
} else {
// Narrowed to Employee
console.log(`Started on: ${user.startDate.toLocaleDateString()}`);
}
}
Here:
- The
inoperator checks if"privileges"is a key within theuserobject. - This is a very common pattern when handling API responses that might return different data structures based on the user's role.
Custom Type Guards
Sometimes built-in operators aren't enough, especially for complex validation logic. You can create a function that returns a Type Predicate. A type predicate takes the form parameterName is Type.
interface Bird {
fly: () => void;
}
interface Fish {
swim: () => void;
}
// This is a Custom Type Guard
function isBird(pet: Bird | Fish): pet is Bird {
return (pet as Bird).fly !== undefined;
}
function move(pet: Bird | Fish) {
if (isBird(pet)) {
pet.fly(); // TypeScript is certain this is a Bird
} else {
pet.swim(); // TypeScript is certain this is a Fish
}
}
In this logic:
- The function returns a boolean, but the return type
pet is Birdtells TypeScript: "If this function returns true, treat the variable as a Bird in the calling scope." - This makes your code much more readable and modular.
(string | null)[], you can use a custom type guard with .filter() to result in a clean string[] array that TypeScript recognizes.
Type Guards with Union Types
Type guards are most frequently used to handle "Discriminated Unions." This is a pattern where every type in a union has a common property (usually called kind or type) with a literal value.
interface SuccessResponse {
status: 'success';
data: string;
}
interface ErrorResponse {
status: 'error';
message: string;
}
function handleResponse(res: SuccessResponse | ErrorResponse) {
if (res.status === 'success') {
console.log("Data:", res.data);
} else {
console.log("Error:", res.message);
}
}
This is often considered the "gold standard" of type narrowing in TypeScript because it is explicit, easy to read, and works perfectly with switch statements.
Summary
- Type Guards: Essential tools for narrowing down Union types to a specific subtype, ensuring your code doesn't crash at runtime.
- Built-in Type Guards: Use
typeoffor primitives (string, number, etc.),instanceoffor class instances, andinfor checking object properties. - Custom Type Guards: Use functions with the
parameter is Typesyntax for reusable, complex type-checking logic. - Type Safety: By using guards, you enable TypeScript's powerful static analysis to catch errors before you ever run your code.
Mastering type guards will make your TypeScript code significantly more robust and self-documenting, as the code itself explains the logic of your data structures.