TypeScript Type Guards
1. typeof and instanceof Type Guards
| Guard Type | Syntax | Use Case | Narrows To |
|---|---|---|---|
| typeof | typeof x === 'type' |
Check primitive types and functions at runtime | string, number, boolean, symbol, undefined, object, function |
| instanceof | x instanceof Class |
Check if object is instance of a class | Specific class type |
| typeof null | typeof x === 'object' |
⚠️ Returns 'object' for null (JavaScript quirk) | Need additional null check |
| typeof array | typeof [] === 'object' |
⚠️ Arrays return 'object', use Array.isArray() | Use Array.isArray() instead |
Example: typeof type guards
function processValue(value: string | number | boolean) {
if (typeof value === 'string') {
// Type narrowed to string
return value.toUpperCase();
}
if (typeof value === 'number') {
// Type narrowed to number
return value.toFixed(2);
}
// Type narrowed to boolean
return value ? 'yes' : 'no';
}
// Function type check
function callIfFunction(fn: unknown) {
if (typeof fn === 'function') {
// Type narrowed to Function
fn();
}
}
// Handle null and object
function processObject(value: unknown) {
if (typeof value === 'object' && value !== null) {
// Type narrowed to object (excluding null)
console.log('Is object:', value);
}
}
Example: instanceof type guards
class Dog {
bark() { console.log('Woof!'); }
}
class Cat {
meow() { console.log('Meow!'); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
// Type narrowed to Dog
animal.bark();
} else {
// Type narrowed to Cat
animal.meow();
}
}
// Built-in classes
function processDate(value: Date | string) {
if (value instanceof Date) {
// Type narrowed to Date
return value.toISOString();
}
// Type narrowed to string
return new Date(value).toISOString();
}
// Error handling
function handleError(error: unknown) {
if (error instanceof Error) {
// Type narrowed to Error
console.error(error.message, error.stack);
} else {
console.error('Unknown error:', error);
}
}
2. User-Defined Type Guards (is keyword)
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| Type Predicate | x is Type |
Custom function returning type predicate - tells TS about narrowing | Complex validation logic |
| Return Type | function(x): x is T |
Return type must be boolean with is predicate |
Type guard function signature |
| Parameter Name | paramName is Type |
Must match parameter name in function signature | Type guard reference |
| Narrowing Scope | After guard call | Type narrowed only in if/else blocks after guard check | Control flow analysis |
Example: Basic type predicates
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
// User-defined type guard
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
// Type narrowed to Fish
animal.swim();
} else {
// Type narrowed to Bird
animal.fly();
}
}
// String validation guard
function isValidEmail(str: string): str is string {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
function sendEmail(email: string) {
if (isValidEmail(email)) {
// Type still string, but validated
console.log(`Sending to ${email}`);
} else {
throw new Error('Invalid email');
}
}
Example: Narrowing unknown types
// Check if value is string
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Check if value is number
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
// Check if value is array
function isArray<T>(value: unknown): value is T[] {
return Array.isArray(value);
}
// Complex object validation
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as any).id === 'number' &&
typeof (value as any).name === 'string' &&
typeof (value as any).email === 'string'
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// Type narrowed to User
console.log(data.name, data.email);
}
}
Example: Generic type guards
// Generic array element guard
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(guard);
}
// Usage with primitives
const data: unknown = [1, 2, 3];
if (isArrayOf(data, isNumber)) {
// Type narrowed to number[]
data.forEach(n => console.log(n.toFixed(2)));
}
// Nullable type guard
function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
function processValue(value: string | null | undefined) {
if (isNonNullable(value)) {
// Type narrowed to string
return value.toUpperCase();
}
}
3. Assertion Functions (asserts keyword)
| Feature | Syntax | Description | Behavior |
|---|---|---|---|
| asserts condition | asserts condition |
Assert boolean condition - throws if false | Narrows type in rest of scope |
| asserts x is Type | asserts x is T |
Assert parameter is specific type - throws if not | Type narrowing after call |
| Return Type | void or never |
Assertion functions return void or throw (never returns) | Side effect: type narrowing |
| Throw Behavior | Must throw on failure | Function must throw or process.exit on assertion failure | Compiler assumes success |
Example: Basic assertion functions
// Assert condition is true
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg || 'Assertion failed');
}
}
function processValue(value: string | null) {
assert(value !== null, 'Value cannot be null');
// After assert, type narrowed to string
console.log(value.toUpperCase());
}
// Assert type
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
function handleInput(input: unknown) {
assertIsString(input);
// After assertion, type narrowed to string
console.log(input.trim());
}
Example: Non-null assertions
// Assert value is not null or undefined
function assertNonNullable<T>(
value: T,
message?: string
): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(message || 'Value is null or undefined');
}
}
function getUser(id: string): User | null {
// ... database lookup
return null;
}
function displayUser(id: string) {
const user = getUser(id);
assertNonNullable(user, 'User not found');
// Type narrowed from User | null to User
console.log(user.name, user.email);
}
// Assert array has elements
function assertNotEmpty<T>(
array: T[],
message?: string
): asserts array is [T, ...T[]] {
if (array.length === 0) {
throw new Error(message || 'Array is empty');
}
}
function processItems(items: string[]) {
assertNotEmpty(items);
// Type narrowed to [string, ...string[]] (non-empty tuple)
const first = items[0]; // Type is string, not string | undefined
}
Example: Complex type assertions
interface User {
id: number;
name: string;
email: string;
}
// Assert object matches interface
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== 'object' ||
value === null ||
typeof (value as any).id !== 'number' ||
typeof (value as any).name !== 'string' ||
typeof (value as any).email !== 'string'
) {
throw new Error('Invalid user object');
}
}
// API response handling
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
assertIsUser(data);
// Type narrowed to User after assertion
return data;
}
// Discriminated union assertion
type Success = { status: 'success'; data: string };
type Failure = { status: 'error'; error: string };
type Result = Success | Failure;
function assertSuccess(result: Result): asserts result is Success {
if (result.status !== 'success') {
throw new Error('Expected success result');
}
}
function handleResult(result: Result) {
assertSuccess(result);
// Type narrowed to Success
console.log(result.data);
}
Note: Use type predicates (x is T) when you want to check and
branch. Use assertion functions (asserts x is T) when failure should terminate
execution. Assertions are great for preconditions and invariants.
4. Discriminated Union Type Guards
| Component | Description | Example | Purpose |
|---|---|---|---|
| Discriminant Property | Common literal property across union members | type: 'success' | 'error' |
Distinguish union variants |
| Literal Type | Unique literal value per variant | 'success', 'error' |
Enable type narrowing |
| Switch Statement | Pattern match on discriminant | switch(value.type) { ... } |
Exhaustive checking |
| Exhaustiveness Check | Ensure all cases handled | const _: never = value; |
Compile-time completeness |
Example: Basic discriminated unions
// Tagged union with discriminant property
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'square'; size: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// Type narrowed to { kind: 'circle'; radius: number }
return Math.PI * shape.radius ** 2;
case 'rectangle':
// Type narrowed to { kind: 'rectangle'; width: number; height: number }
return shape.width * shape.height;
case 'square':
// Type narrowed to { kind: 'square'; size: number }
return shape.size ** 2;
default:
// Exhaustiveness check
const _exhaustive: never = shape;
throw new Error('Unknown shape');
}
}
// If statements work too
function describe(shape: Shape): string {
if (shape.kind === 'circle') {
return `Circle with radius ${shape.radius}`;
}
if (shape.kind === 'rectangle') {
return `Rectangle ${shape.width}x${shape.height}`;
}
return `Square ${shape.size}x${shape.size}`;
}
Example: API response handling
// Success/Error discriminated union
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
console.log('Loading...');
break;
case 'success':
// response.data is accessible and typed as T
console.log('Data:', response.data);
break;
case 'error':
// response.error is accessible and typed as string
console.error('Error:', response.error);
break;
}
}
// Redux-style action types
type Action =
| { type: 'INCREMENT'; by: number }
| { type: 'DECREMENT'; by: number }
| { type: 'RESET' }
| { type: 'SET'; value: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + action.by;
case 'DECREMENT':
return state - action.by;
case 'RESET':
return 0;
case 'SET':
return action.value;
default:
// Exhaustiveness check catches missing cases
const _: never = action;
return state;
}
}
Example: Multiple discriminants
// Multiple discriminant properties
type NetworkState =
| { state: 'idle' }
| { state: 'loading'; progress: number }
| { state: 'success'; data: string; cached: boolean }
| { state: 'error'; error: string; retryable: boolean };
function handleNetworkState(state: NetworkState) {
// Primary discriminant
switch (state.state) {
case 'idle':
console.log('Idle');
break;
case 'loading':
console.log(`Loading: ${state.progress}%`);
break;
case 'success':
// Secondary check on cached
if (state.cached) {
console.log('From cache:', state.data);
} else {
console.log('Fresh data:', state.data);
}
break;
case 'error':
if (state.retryable) {
console.log('Retrying...', state.error);
} else {
console.error('Fatal error:', state.error);
}
break;
}
}
// Nested discriminated unions
type PaymentMethod =
| { type: 'card'; card: { number: string; cvv: string } }
| { type: 'paypal'; email: string }
| { type: 'crypto'; wallet: string; currency: 'BTC' | 'ETH' };
function processPayment(method: PaymentMethod) {
if (method.type === 'crypto') {
// Further narrow by currency
if (method.currency === 'BTC') {
console.log('Bitcoin payment to', method.wallet);
} else {
console.log('Ethereum payment to', method.wallet);
}
}
}
5. in Operator for Property Existence Checking
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| in Operator | 'prop' in obj |
Check if property exists in object at runtime | Narrow union types by properties |
| Type Narrowing | After in check |
TypeScript narrows to types containing property | Discriminate union members |
| Optional Properties | property?: type |
Property might be undefined but still "in" object | Check existence vs value |
| Inherited Properties | Prototype chain | in checks own and inherited properties |
Use hasOwnProperty for own only |
Example: Basic in operator narrowing
interface Cat {
meow: () => void;
purr: () => void;
}
interface Dog {
bark: () => void;
fetch: () => void;
}
type Pet = Cat | Dog;
function interact(pet: Pet) {
if ('meow' in pet) {
// Type narrowed to Cat
pet.meow();
pet.purr();
} else {
// Type narrowed to Dog
pet.bark();
pet.fetch();
}
}
// Multiple property checks
function feedPet(pet: Pet) {
if ('bark' in pet) {
// Dog
console.log('Feeding dog');
} else if ('meow' in pet) {
// Cat
console.log('Feeding cat');
}
}
Example: Complex union narrowing
type Vehicle =
| { type: 'car'; wheels: 4; engine: string }
| { type: 'bike'; wheels: 2; pedals: boolean }
| { type: 'boat'; propeller: string };
function describe(vehicle: Vehicle) {
// Check for specific property
if ('engine' in vehicle) {
// Narrowed to car
console.log(`Car with ${vehicle.wheels} wheels and ${vehicle.engine}`);
} else if ('pedals' in vehicle) {
// Narrowed to bike
console.log(`Bike with ${vehicle.pedals ? 'pedals' : 'no pedals'}`);
} else {
// Narrowed to boat
console.log(`Boat with ${vehicle.propeller}`);
}
}
// API response with optional fields
interface SuccessResponse {
success: true;
data: string;
}
interface ErrorResponse {
success: false;
error: string;
code?: number;
}
type Response = SuccessResponse | ErrorResponse;
function handleResponse(response: Response) {
if ('error' in response) {
// Narrowed to ErrorResponse
console.error('Error:', response.error);
if ('code' in response && response.code) {
console.error('Error code:', response.code);
}
} else {
// Narrowed to SuccessResponse
console.log('Data:', response.data);
}
}
Example: Combining with other guards
// Combining in with typeof
function processValue(value: unknown) {
if (typeof value === 'object' && value !== null) {
if ('length' in value) {
// Likely an array or array-like
console.log('Length:', (value as any).length);
}
if ('toString' in value) {
// Has toString method (most objects)
console.log('String:', value.toString());
}
}
}
// Check method existence
interface Flyable {
fly: () => void;
}
interface Swimmable {
swim: () => void;
}
type Creature = Flyable | Swimmable | (Flyable & Swimmable);
function move(creature: Creature) {
const canFly = 'fly' in creature;
const canSwim = 'swim' in creature;
if (canFly && canSwim) {
console.log('Can fly and swim');
creature.fly();
creature.swim();
} else if (canFly) {
creature.fly();
} else {
creature.swim();
}
}
6. Control Flow Analysis and Type Narrowing
| Technique | Description | Example Pattern | Reliability |
|---|---|---|---|
| Truthiness Narrowing | if(x) narrows from union with null/undefined/false/0/"" | if (x) { /* x is truthy */ } |
Simple but imprecise |
| Equality Narrowing | === and !== narrow to/from specific values | if (x === null) { } |
Precise |
| Assignment Narrowing | Type narrowed after assignment in same scope | x = 'string'; /* x is string */ |
Within scope only |
| Early Return | Return/throw narrows type in rest of function | if (!x) return; /* x is truthy */ |
Very reliable |
| Switch Fallthrough | Type narrowed through switch cases | switch(x.type) { case 'a': } |
Reliable with discriminants |
| Exception Narrowing | throw removes possibility from control flow | if (!x) throw Error(); |
Reliable |
Example: Truthiness and equality narrowing
// Truthiness narrowing
function processValue(value: string | null | undefined) {
if (value) {
// Type narrowed to string (null and undefined are falsy)
console.log(value.toUpperCase());
}
}
// Be careful with 0 and empty string
function processNumber(value: number | null) {
if (value) {
// ⚠️ 0 is falsy, so this might not work as expected
console.log(value.toFixed(2));
}
// Better: explicit null check
if (value !== null) {
// Type narrowed to number (includes 0)
console.log(value.toFixed(2));
}
}
// Equality narrowing
function compare(x: string | number, y: string | number) {
if (x === y) {
// Both are same type (string or number)
console.log(x, y);
}
}
// Discriminant equality
type Result =
| { success: true; value: number }
| { success: false; error: string };
function handle(result: Result) {
if (result.success === true) {
// Narrowed to success case
console.log(result.value);
} else {
// Narrowed to error case
console.log(result.error);
}
}
Example: Control flow with early returns
// Early return pattern
function processUser(user: User | null | undefined): string {
// Guard clause
if (!user) {
return 'No user';
}
// After guard, user is definitely User
if (!user.email) {
return 'No email';
}
// user and user.email both exist
return user.email.toLowerCase();
}
// Multiple guards
function validateInput(input: string | null | undefined): string {
if (input === null) {
throw new Error('Input is null');
}
if (input === undefined) {
throw new Error('Input is undefined');
}
// Type narrowed to string
if (input.length === 0) {
throw new Error('Input is empty');
}
// Type is string with length > 0
return input.trim();
}
// Nested narrowing
function processData(data: unknown): number {
if (typeof data !== 'object' || data === null) {
throw new Error('Not an object');
}
// Type narrowed to object
if (!('value' in data)) {
throw new Error('No value property');
}
const value = (data as any).value;
if (typeof value !== 'number') {
throw new Error('Value is not a number');
}
// Type narrowed to number
return value;
}
Example: Assignment and control flow
// Type narrowing through assignment
function demo() {
let x: string | number;
x = 'hello';
// Type narrowed to string
console.log(x.toUpperCase());
x = 42;
// Type narrowed to number
console.log(x.toFixed(2));
}
// Conditional assignment
function processOptional(value?: string) {
let result: string;
if (value !== undefined) {
result = value;
// result is string, value is string
} else {
result = 'default';
}
// result is always string here
console.log(result.toUpperCase());
}
// Type guards with assignments
function findUser(id: string): User | undefined {
// ... lookup logic
return undefined;
}
function displayUser(id: string) {
const user = findUser(id);
if (!user) {
console.log('User not found');
return;
}
// user is definitely User here
console.log(user.name);
// Can reassign
const admin: User | Admin = user;
if ('permissions' in admin) {
// Narrowed to Admin
console.log(admin.permissions);
}
}
Example: Complex control flow scenarios
// Unreachable code detection
function handleValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
// Type narrowed to number
return value.toFixed(2);
// This would be detected as unreachable
// console.log('Never executed');
}
// Exhaustiveness with control flow
type Status = 'pending' | 'approved' | 'rejected';
function processStatus(status: Status): string {
if (status === 'pending') {
return 'Waiting...';
}
if (status === 'approved') {
return 'Approved!';
}
if (status === 'rejected') {
return 'Rejected!';
}
// TypeScript knows this is unreachable
// status has type 'never' here
const _exhaustive: never = status;
return _exhaustive;
}
// Type narrowing with logical operators
function process(value: string | null | undefined) {
// Using || for defaults
const str = value || 'default';
// Type is string
// Using ?? for null/undefined only
const str2 = value ?? 'default';
// Type is string
// Using && for chaining
const result = value && value.toUpperCase();
// Type is string | null | undefined | false
}
Note: TypeScript's control flow analysis tracks type narrowing through:
- Conditional statements (if/else, switch, ternary)
- Early exits (return, throw, break, continue)
- Type guards (typeof, instanceof, in, user-defined)
- Assignments within the same scope
strictNullChecks for better control flow analysis.
Warning: Control flow analysis doesn't work across:
- Function boundaries - type guards don't propagate to inner functions
- Async boundaries - type narrowing lost after await
- Array methods - callbacks don't maintain outer scope narrowing
- Object mutations - changing properties doesn't narrow types
Type Guards Summary
- typeof/instanceof - Built-in guards for primitives and classes
- Type predicates (x is T) - Custom guards that return boolean
- Assertion functions (asserts x is T) - Guards that throw on failure
- Discriminated unions - Use literal property to distinguish variants
- in operator - Check property existence to narrow unions
- Control flow - TypeScript tracks narrowing through conditionals and exits