Chapter 8: Type Guards & Assertions
Authored by syscook.dev
What are Type Guards and Assertions?
Type guards are expressions that perform runtime checks to narrow down the type of a variable within a specific scope. Type assertions tell the TypeScript compiler to treat a value as a specific type, bypassing the compiler's type checking.
Key Concepts:
- Type Guards: Runtime checks that narrow types
- Type Assertions: Compile-time type overrides
- User-Defined Type Guards: Custom type guard functions
- Built-in Type Guards: typeof, instanceof, in operator
- Type Predicates: Functions that return type predicates
- Assertion Signatures: Functions that assert types
Why Use Type Guards and Assertions?
1. Runtime Type Safety
Type guards provide runtime type checking to ensure type safety.
// Without type guards - potential runtime errors
function processValue(value: unknown) {
// This could fail at runtime
return value.toUpperCase(); // Error: Object is of type 'unknown'
}
// With type guards - safe runtime checking
function processValue(value: unknown) {
if (typeof value === 'string') {
return value.toUpperCase(); // Type is narrowed to string
}
throw new Error('Value must be a string');
}
// Usage
try {
const result = processValue("hello");
console.log("Result:", result);
} catch (error) {
console.error("Error:", error.message);
}
Output:
Result: HELLO
2. Type Narrowing
Type guards narrow the type of variables within specific scopes.
// Type narrowing with type guards
function processData(data: string | number | boolean) {
if (typeof data === 'string') {
// data is narrowed to string
console.log("String length:", data.length);
console.log("Uppercase:", data.toUpperCase());
} else if (typeof data === 'number') {
// data is narrowed to number
console.log("Number squared:", data * data);
console.log("Is integer:", Number.isInteger(data));
} else {
// data is narrowed to boolean
console.log("Boolean value:", data);
console.log("Negated:", !data);
}
}
// Usage
processData("Hello World");
processData(42);
processData(true);
Output:
String length: 11
Uppercase: HELLO WORLD
Number squared: 1764
Is integer: true
Boolean value: true
Negated: false
How to Use Type Guards and Assertions?
1. Built-in Type Guards
typeof Operator
// typeof type guard
function handleValue(value: unknown) {
if (typeof value === 'string') {
console.log("String:", value.toUpperCase());
} else if (typeof value === 'number') {
console.log("Number:", value.toFixed(2));
} else if (typeof value === 'boolean') {
console.log("Boolean:", value ? "true" : "false");
} else if (typeof value === 'object') {
if (value === null) {
console.log("Null value");
} else {
console.log("Object:", Object.keys(value));
}
} else if (typeof value === 'function') {
console.log("Function:", value.name);
} else {
console.log("Other type:", typeof value);
}
}
// Usage
handleValue("Hello");
handleValue(42.567);
handleValue(true);
handleValue({ name: "John", age: 30 });
handleValue(null);
handleValue(() => console.log("test"));
handleValue(undefined);
Output:
String: HELLO
Number: 42.57
Boolean: true
Object: ['name', 'age']
Null value
Function: test
Other type: undefined
instanceof Operator
// instanceof type guard
class Animal {
constructor(public name: string) {}
makeSound(): void {
console.log("Some animal sound");
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
makeSound(): void {
console.log("Woof! Woof!");
}
fetch(): void {
console.log(`${this.name} is fetching the ball`);
}
}
class Cat extends Animal {
constructor(name: string, public color: string) {
super(name);
}
makeSound(): void {
console.log("Meow! Meow!");
}
purr(): void {
console.log(`${this.name} is purring`);
}
}
function handleAnimal(animal: Animal) {
console.log(`Handling animal: ${animal.name}`);
animal.makeSound();
if (animal instanceof Dog) {
// animal is narrowed to Dog
console.log(`Breed: ${animal.breed}`);
animal.fetch();
} else if (animal instanceof Cat) {
// animal is narrowed to Cat
console.log(`Color: ${animal.color}`);
animal.purr();
}
}
// Usage
const dog = new Dog("Buddy", "Golden Retriever");
const cat = new Cat("Whiskers", "Orange");
handleAnimal(dog);
console.log("---");
handleAnimal(cat);
Output:
Handling animal: Buddy
Woof! Woof!
Breed: Golden Retriever
Buddy is fetching the ball
---
Handling animal: Whiskers
Meow! Meow!
Color: Orange
Whiskers is purring
in Operator
// in operator type guard
interface Bird {
fly(): void;
feathers: boolean;
}
interface Fish {
swim(): void;
fins: boolean;
}
function handleCreature(creature: Bird | Fish) {
if ('fly' in creature) {
// creature is narrowed to Bird
console.log("It's a bird!");
creature.fly();
console.log("Has feathers:", creature.feathers);
} else if ('swim' in creature) {
// creature is narrowed to Fish
console.log("It's a fish!");
creature.swim();
console.log("Has fins:", creature.fins);
}
}
// Usage
const bird: Bird = {
fly: () => console.log("Flying high!"),
feathers: true
};
const fish: Fish = {
swim: () => console.log("Swimming deep!"),
fins: true
};
handleCreature(bird);
console.log("---");
handleCreature(fish);
Output:
It's a bird!
Flying high!
Has feathers: true
---
It's a fish!
Swimming deep!
Has fins: true
2. User-Defined Type Guards
Type Predicate Functions
// User-defined type guard with type predicate
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
email: string;
permissions: string[];
}
type UserOrAdmin = User | Admin;
// Type guard function
function isAdmin(user: UserOrAdmin): user is Admin {
return 'permissions' in user;
}
function isUser(user: UserOrAdmin): user is User {
return !('permissions' in user);
}
function handleUser(user: UserOrAdmin) {
if (isAdmin(user)) {
// user is narrowed to Admin
console.log(`Admin: ${user.name}`);
console.log("Permissions:", user.permissions);
} else if (isUser(user)) {
// user is narrowed to User
console.log(`User: ${user.name}`);
console.log("Email:", user.email);
}
}
// Usage
const regularUser: User = {
id: 1,
name: "John",
email: "[email protected]"
};
const adminUser: Admin = {
id: 2,
name: "Alice",
email: "[email protected]",
permissions: ["read", "write", "delete"]
};
handleUser(regularUser);
console.log("---");
handleUser(adminUser);
Output:
User: John
Email: [email protected]
---
Admin: Alice
Permissions: ['read', 'write', 'delete']
Complex Type Guards
// Complex type guard for API responses
interface SuccessResponse<T> {
success: true;
data: T;
timestamp: string;
}
interface ErrorResponse {
success: false;
error: string;
code: number;
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
// Type guard functions
function isSuccessResponse<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
return response.success === true;
}
function isErrorResponse<T>(response: ApiResponse<T>): response is ErrorResponse {
return response.success === false;
}
// Generic type guard
function isValidResponse<T>(response: any): response is ApiResponse<T> {
return typeof response === 'object' &&
response !== null &&
typeof response.success === 'boolean' &&
(response.success ? 'data' in response : 'error' in response);
}
function handleApiResponse<T>(response: ApiResponse<T>) {
if (isSuccessResponse(response)) {
// response is narrowed to SuccessResponse<T>
console.log("Success!");
console.log("Data:", response.data);
console.log("Timestamp:", response.timestamp);
} else if (isErrorResponse(response)) {
// response is narrowed to ErrorResponse
console.log("Error!");
console.log("Error message:", response.error);
console.log("Error code:", response.code);
}
}
// Usage
const successResponse: ApiResponse<string> = {
success: true,
data: "Hello World",
timestamp: new Date().toISOString()
};
const errorResponse: ApiResponse<string> = {
success: false,
error: "Not Found",
code: 404
};
handleApiResponse(successResponse);
console.log("---");
handleApiResponse(errorResponse);
Output:
Success!
Data: Hello World
Timestamp: 2024-01-15T10:30:00.000Z
---
Error!
Error message: Not Found
Error code: 404
3. Type Assertions
Basic Type Assertions
// Type assertions
function getValue(): unknown {
return "Hello World";
}
// Type assertion with 'as'
const value = getValue() as string;
console.log("Value length:", value.length);
console.log("Uppercase:", value.toUpperCase());
// Type assertion with angle brackets
const value2 = <string>getValue();
console.log("Value2 length:", value2.length);
// Type assertion with non-null assertion
function getElement(): HTMLElement | null {
return document.getElementById('myElement');
}
const element = getElement()!; // Non-null assertion
console.log("Element:", element);
// Type assertion with any
const anyValue: any = "This is a string";
const stringValue = anyValue as string;
console.log("String value:", stringValue);
Output:
Value length: 11
Uppercase: HELLO WORLD
Value2 length: 11
Element: null
String value: This is a string
Type Assertions with Interfaces
// Type assertions with interfaces
interface User {
id: number;
name: string;
email: string;
}
interface Admin extends User {
permissions: string[];
}
// Type assertion to more specific type
function getUser(): User {
return {
id: 1,
name: "John",
email: "[email protected]"
};
}
const user = getUser();
const admin = user as Admin; // Type assertion
console.log("Admin:", admin);
// Type assertion with unknown
function getUnknownData(): unknown {
return {
id: 2,
name: "Alice",
email: "[email protected]",
permissions: ["read", "write"]
};
}
const unknownData = getUnknownData();
const typedData = unknownData as Admin;
console.log("Typed data:", typedData);
Output:
Admin: { id: 1, name: 'John', email: '[email protected]' }
Typed data: { id: 2, name: 'Alice', email: '[email protected]', permissions: ['read', 'write'] }
4. Assertion Signatures
// Assertion signatures
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new Error('Value is not a number');
}
}
function assertIsArray<T>(value: unknown): asserts value is T[] {
if (!Array.isArray(value)) {
throw new Error('Value is not an array');
}
}
// Usage
function processData(data: unknown) {
assertIsString(data);
// data is now narrowed to string
console.log("String length:", data.length);
console.log("Uppercase:", data.toUpperCase());
}
function processNumbers(numbers: unknown) {
assertIsArray<number>(numbers);
// numbers is now narrowed to number[]
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log("Sum:", sum);
}
// Usage
try {
processData("Hello World");
processNumbers([1, 2, 3, 4, 5]);
} catch (error) {
console.error("Error:", error.message);
}
Output:
String length: 11
Uppercase: HELLO WORLD
Sum: 15
Practical Examples
1. API Response Handler
// API response handler with type guards
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// Type guard for API response
function isApiResponse<T>(response: any): response is ApiResponse<T> {
return typeof response === 'object' &&
response !== null &&
typeof response.data !== 'undefined' &&
typeof response.status === 'number' &&
typeof response.message === 'string';
}
// Type guard for User
function isUser(data: any): data is User {
return typeof data === 'object' &&
data !== null &&
typeof data.id === 'number' &&
typeof data.name === 'string' &&
typeof data.email === 'string';
}
// Type guard for Product
function isProduct(data: any): data is Product {
return typeof data === 'object' &&
data !== null &&
typeof data.id === 'number' &&
typeof data.title === 'string' &&
typeof data.price === 'number';
}
// API handler
function handleApiResponse(response: unknown) {
if (isApiResponse<User>(response)) {
console.log("API Response received");
console.log("Status:", response.status);
console.log("Message:", response.message);
if (isUser(response.data)) {
console.log("User data:", response.data);
console.log("User name:", response.data.name);
console.log("User email:", response.data.email);
}
} else {
console.log("Invalid API response format");
}
}
// Usage
const userResponse: ApiResponse<User> = {
data: {
id: 1,
name: "John",
email: "[email protected]"
},
status: 200,
message: "Success"
};
handleApiResponse(userResponse);
Output:
API Response received
Status: 200
Message: Success
User data: { id: 1, name: 'John', email: '[email protected]' }
User name: John
User email: [email protected]
2. Form Validation System
// Form validation with type guards
interface FormField {
value: unknown;
error?: string;
touched: boolean;
}
interface FormData {
name: FormField;
email: FormField;
age: FormField;
}
// Type guards for form validation
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function isNotEmpty(value: string): boolean {
return value.trim().length > 0;
}
function isPositiveNumber(value: number): boolean {
return value > 0;
}
// Form validator
class FormValidator {
validateField(field: FormField, fieldName: string): FormField {
const { value, touched } = field;
if (!touched) {
return field;
}
let error: string | undefined;
switch (fieldName) {
case 'name':
if (!isString(value) || !isNotEmpty(value)) {
error = 'Name is required and must be a string';
}
break;
case 'email':
if (!isString(value) || !isNotEmpty(value)) {
error = 'Email is required and must be a string';
} else if (!isEmail(value)) {
error = 'Email must be a valid email address';
}
break;
case 'age':
if (!isNumber(value)) {
error = 'Age must be a number';
} else if (!isPositiveNumber(value)) {
error = 'Age must be a positive number';
}
break;
}
return { ...field, error };
}
validateForm(formData: FormData): FormData {
return {
name: this.validateField(formData.name, 'name'),
email: this.validateField(formData.email, 'email'),
age: this.validateField(formData.age, 'age')
};
}
}
// Usage
const validator = new FormValidator();
const formData: FormData = {
name: { value: "John Doe", touched: true },
email: { value: "[email protected]", touched: true },
age: { value: 25, touched: true }
};
const validatedForm = validator.validateForm(formData);
console.log("Validated form:");
console.log("Name:", validatedForm.name);
console.log("Email:", validatedForm.email);
console.log("Age:", validatedForm.age);
Output:
Validated form:
Name: { value: 'John Doe', touched: true }
Email: { value: '[email protected]', touched: true }
Age: { value: 25, touched: true }
Best Practices
1. Use Type Guards for Runtime Safety
// Good: Use type guards for runtime safety
function processData(data: unknown) {
if (typeof data === 'string') {
// Safe to use string methods
return data.toUpperCase();
}
throw new Error('Expected string');
}
2. Create Reusable Type Guard Functions
// Good: Create reusable type guard functions
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isArray<T>(value: unknown): value is T[] {
return Array.isArray(value);
}
3. Use Type Assertions Sparingly
// Good: Use type assertions when you're certain about the type
const element = document.getElementById('myElement') as HTMLElement;
// Avoid: Overusing type assertions
const value = someValue as any; // Too permissive
4. Combine Type Guards with Error Handling
// Good: Combine type guards with error handling
function safeProcessData(data: unknown) {
if (typeof data === 'string') {
return data.toUpperCase();
}
throw new Error(`Expected string, got ${typeof data}`);
}
Common Pitfalls and Solutions
1. Type Guard Side Effects
// ❌ Problem: Type guard with side effects
function isString(value: unknown): value is string {
console.log('Checking if string'); // Side effect
return typeof value === 'string';
}
// ✅ Solution: Keep type guards pure
function isString(value: unknown): value is string {
return typeof value === 'string';
}
2. Incorrect Type Assertions
// ❌ Problem: Incorrect type assertion
const value = 42 as string; // Runtime error
// ✅ Solution: Use proper type checking
function processValue(value: unknown) {
if (typeof value === 'string') {
return value.toUpperCase();
}
throw new Error('Expected string');
}
3. Complex Type Guard Logic
// ❌ Problem: Too complex type guard
function isComplexType(value: unknown): value is ComplexType {
return typeof value === 'object' &&
value !== null &&
'prop1' in value &&
'prop2' in value &&
// ... many more checks
}
// ✅ Solution: Break down into simpler guards
function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null;
}
function hasProperty<T extends object, K extends PropertyKey>(
obj: T,
prop: K
): obj is T & Record<K, unknown> {
return prop in obj;
}
Conclusion
Type guards and assertions in TypeScript provide essential tools for runtime type safety and type narrowing. By understanding:
- What type guards and assertions are and their purposes
- Why they're important for runtime safety and type narrowing
- How to use built-in and create custom type guards
You can create robust applications that handle dynamic data safely. Type guards enable runtime type checking while maintaining compile-time type safety, and type assertions provide controlled ways to override the compiler's type checking when necessary.
Next Steps
- Practice creating custom type guard functions
- Explore advanced type guard patterns
- Learn about assertion signatures
- Move on to Chapter 9: Async Programming
This tutorial is part of the TypeScript Mastery series by syscook.dev