What are type guards? Implement a custom type guard function and explain how TypeScript narrows types.
Quick Answer
Type guards are expressions that perform runtime checks to narrow down the type of a variable within a specific scope. They help TypeScript understand the actual type of a value at runtime, enabling better type safety and IntelliSense.
Detailed Answer
What are type guards? Implement a custom type guard function and explain how TypeScript narrows types.
Answer: Type guards are expressions that perform runtime checks to narrow down the type of a variable within a specific scope. They help TypeScript understand the actual type of a value at runtime, enabling better type safety and IntelliSense.
How Type Narrowing Works:
TypeScript uses control flow analysis to narrow types based on certain conditions. When a type guard returns true, TypeScript narrows the type in the subsequent code block.
Built-in Type Guards:
- typeof Guards:
function processValue(value: string | number) {
if (typeof value === 'string') {
// TypeScript knows value is string here
console.log(value.toUpperCase()); // OK
console.log(value.length); // OK
} else {
// TypeScript knows value is number here
console.log(value.toFixed(2)); // OK
console.log(value * 2); // OK
}
}
- instanceof Guards:
function processError(error: Error | string) {
if (error instanceof Error) {
// TypeScript knows error is Error here
console.log(error.message);
console.log(error.stack);
} else {
// TypeScript knows error is string here
console.log(error.toUpperCase());
}
}
- in Guards:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
// TypeScript knows animal is Bird
animal.fly();
} else {
// TypeScript knows animal is Fish
animal.swim();
}
}
- Equality Guards:
function processValue(value: string | null | undefined) {
if (value === null) {
// TypeScript knows value is null
console.log('Value is null');
} else if (value === undefined) {
// TypeScript knows value is undefined
console.log('Value is undefined');
} else {
// TypeScript knows value is string
console.log(value.toUpperCase());
}
}
Custom Type Guards:
Type guards are functions that return a type predicate: value is Type.
// Basic custom type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processUnknown(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string
console.log(value.toUpperCase());
}
}
// More complex type guard
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
permissions: string[];
}
function isUser(obj: any): obj is User {
return obj &&
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
!('permissions' in obj);
}
function isAdmin(obj: any): obj is Admin {
return obj &&
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
Array.isArray(obj.permissions);
}
function processPerson(person: User | Admin) {
if (isUser(person)) {
// TypeScript knows person is User
console.log(`User email: ${person.email}`);
} else if (isAdmin(person)) {
// TypeScript knows person is Admin
console.log(`Admin permissions: ${person.permissions.join(', ')}`);
}
}
Advanced Type Guard Patterns:
- Discriminated Union Guards:
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: any;
}
interface ErrorState {
status: 'error';
error: string;
}
type AppState = LoadingState | SuccessState | ErrorState;
function handleState(state: AppState) {
switch (state.status) {
case 'loading':
// TypeScript knows state is LoadingState
console.log('Loading...');
break;
case 'success':
// TypeScript knows state is SuccessState
console.log('Data:', state.data);
break;
case 'error':
// TypeScript knows state is ErrorState
console.log('Error:', state.error);
break;
}
}
- Array Type Guards:
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function processArray(value: unknown) {
if (isStringArray(value)) {
// TypeScript knows value is string[]
value.forEach(str => console.log(str.toUpperCase()));
}
}
- Generic Type Guards:
function isOfType<T>(
value: unknown,
type: string
): value is T {
return typeof value === type;
}
function isNumber(value: unknown): value is number {
return isOfType<number>(value, 'number');
}
function isBoolean(value: unknown): value is boolean {
return isOfType<boolean>(value, 'boolean');
}
- Complex Object Validation:
interface ApiResponse {
success: boolean;
data?: any;
error?: string;
}
function isApiResponse(obj: unknown): obj is ApiResponse {
return (
typeof obj === 'object' &&
obj !== null &&
'success' in obj &&
typeof (obj as any).success === 'boolean'
);
}
function handleApiResponse(response: unknown) {
if (isApiResponse(response)) {
if (response.success) {
// TypeScript knows response.data exists
console.log('Data:', response.data);
} else {
// TypeScript knows response.error exists
console.log('Error:', response.error);
}
}
}
- Branded Types with Type Guards:
// Branded types for additional type safety
type UserId = number & { readonly __brand: 'UserId' };
type ProductId = number & { readonly __brand: 'ProductId' };
function createUserId(id: number): UserId {
if (id <= 0) {
throw new Error('Invalid user ID');
}
return id as UserId;
}
function createProductId(id: number): ProductId {
if (id <= 0) {
throw new Error('Invalid product ID');
}
return id as ProductId;
}
function isUserId(value: unknown): value is UserId {
return typeof value === 'number' && value > 0;
}
function processId(id: UserId | ProductId) {
if (isUserId(id)) {
// TypeScript knows id is UserId
console.log('Processing user ID:', id);
} else {
// TypeScript knows id is ProductId
console.log('Processing product ID:', id);
}
}
Type Guard Best Practices:
- Use Type Predicates:
// Good - uses type predicate
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Avoid - doesn't narrow types
function isString(value: unknown): boolean {
return typeof value === 'string';
}
- Combine Multiple Guards:
function isValidUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj &&
typeof (obj as any).id === 'number' &&
typeof (obj as any).name === 'string' &&
typeof (obj as any).email === 'string'
);
}
- Use Assertion Functions:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Expected string');
}
}
function processValue(value: unknown) {
assertIsString(value);
// TypeScript knows value is string after assertion
console.log(value.toUpperCase());
}
- Create Reusable Guard Libraries:
// Type guard utilities
export const TypeGuards = {
isString: (value: unknown): value is string => typeof value === 'string',
isNumber: (value: unknown): value is number => typeof value === 'number',
isBoolean: (value: unknown): value is boolean => typeof value === 'boolean',
isObject: (value: unknown): value is object => typeof value === 'object' && value !== null,
isArray: (value: unknown): value is unknown[] => Array.isArray(value),
isFunction: (value: unknown): value is Function => typeof value === 'function',
isNull: (value: unknown): value is null => value === null,
isUndefined: (value: unknown): value is undefined => value === undefined,
isNullish: (value: unknown): value is null | undefined => value == null,
};
Common Pitfalls:
- Don't forget the type predicate:
// Wrong - doesn't narrow types
function isString(value: unknown): boolean {
return typeof value === 'string';
}
// Correct - narrows types
function isString(value: unknown): value is string {
return typeof value === 'string';
}
- Be careful with any:
// Avoid using any in type guards
function badGuard(value: any): value is User {
return value.name && value.email; // Too permissive
}
// Better - be specific
function goodGuard(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value &&
typeof (value as any).name === 'string' &&
typeof (value as any).email === 'string'
);
}
Type guards are essential for working with dynamic data, API responses, and user input where types aren't known at compile time. They provide runtime type safety while maintaining TypeScript's compile-time benefits.