Type safety is a cornerstone of TypeScript development, but what happens when you need to ensure type safety at runtime? This is where TypeScript type guards come into play, providing a powerful way to perform runtime type checks while maintaining type safety in your code.
Type guards bridge the gap between TypeScript’s static type system and JavaScript’s dynamic nature, allowing you to write code that is both type-safe and runtime-aware. Let’s dive into how they work and why they’re essential for robust TypeScript applications.
Table of Contents
- What Are Type Guards?
- Built-in Type Guards
- Custom Type Guards
- Type Guards with Union Types
- Best Practices for Type Guards
- Error Handling with Type Guards
- Common Pitfalls
- Conclusion
What Are Type Guards?
Type guards are expressions that perform runtime checks to guarantee the type of a value within a certain scope. When used correctly, they help narrow down the type of a variable or parameter, making your code safer and more predictable.
Built-in Type Guards
TypeScript comes with several built-in type guards that you can use immediately:
typeof Type Guard
The typeof
operator is the most basic type guard:
function processValue(value: string | number) {
if (typeof value === 'string') {
// TypeScript knows value is a string here
return value.toUpperCase();
} else {
// TypeScript knows value is a number here
return value.toFixed(2);
}
}
Code language: JavaScript (javascript)
instanceof Type Guard
Use instanceof
to check if an object is an instance of a specific class:
class Dog {
bark() {
return 'Woof!';
}
}
class Cat {
meow() {
return 'Meow!';
}
}
function makeAnimalSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
return animal.bark();
} else {
return animal.meow();
}
}
Code language: JavaScript (javascript)
Custom Type Guards
While built-in type guards are useful, you often need more specific type checks. Custom type guards allow you to define your own type-checking functions:
interface User {
name: string;
email: string;
}
interface Admin {
name: string;
role: string;
}
function isUser(account: User | Admin): account is User {
return 'email' in account;
}
function handleAccount(account: User | Admin) {
if (isUser(account)) {
// TypeScript knows account is User
console.log(account.email);
} else {
// TypeScript knows account is Admin
console.log(account.role);
}
}
Code language: PHP (php)
Type Guards with Union Types
Type guards are particularly useful when working with union types:
type Result = Success | Error;
interface Success {
success: true;
data: string;
}
interface Error {
success: false;
error: string;
}
function isSuccess(result: Result): result is Success {
return result.success === true;
}
function processResult(result: Result) {
if (isSuccess(result)) {
// Access data safely
console.log(result.data);
} else {
// Handle error
console.error(result.error);
}
}
Code language: JavaScript (javascript)
Best Practices for Type Guards
1. Keep Type Guards Simple
Type guards should focus on a single responsibility and perform clear, straightforward checks:
// Good
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Avoid complex conditions
function isValidUser(user: unknown): user is User {
return typeof user === 'object' && user !== null && 'email' in user && 'name' in user;
}
Code language: JavaScript (javascript)
2. Use Discriminated Unions
When possible, design your types with a discriminant property to make type guards more reliable:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
function isCircle(shape: Shape): shape is Extract<Shape, { kind: 'circle' }> {
return shape.kind === 'circle';
}
Code language: JavaScript (javascript)
3. Combine Type Guards
You can combine multiple type guards for more complex checks:
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.length > 0;
}
Code language: JavaScript (javascript)
Error Handling with Type Guards
Type guards can help make error handling more robust:
function safeParse(data: string): unknown {
try {
return JSON.parse(data);
} catch {
return null;
}
}
function isValidResponse(response: unknown): response is { status: number } {
return (
typeof response === 'object' &&
response !== null &&
'status' in response &&
typeof (response as any).status === 'number'
);
}
function processResponse(data: string) {
const response = safeParse(data);
if (isValidResponse(response)) {
// Safe to use response.status
console.log(response.status);
}
}
Code language: JavaScript (javascript)
Common Pitfalls
Avoid Type Assertions
Instead of using type assertions, prefer type guards:
// Bad
const value: any = getValueFromSomewhere();
const str = value as string;
// Good
const value: unknown = getValueFromSomewhere();
if (typeof value === 'string') {
const str = value; // Type-safe!
}
Code language: JavaScript (javascript)
Conclusion
Type guards are an essential tool in TypeScript development, providing runtime type safety while maintaining the benefits of static typing. By following these patterns and best practices, you can write more reliable and maintainable TypeScript code.
Whether you’re handling API responses, processing user input, or managing complex data structures, type guards help ensure your code behaves correctly at runtime while providing excellent developer experience through TypeScript’s type system.
Start incorporating type guards into your TypeScript projects today, and you’ll notice improved code reliability and better type safety across your applications. Remember to keep your type guards simple, focused, and well-tested for the best results.