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 checkfunction callIfFunction(fn: unknown) { if (typeof fn === 'function') { // Type narrowed to Function fn(); }}// Handle null and objectfunction 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 classesfunction 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 handlingfunction 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 and Predicates
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 guardfunction 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 guardfunction 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 stringfunction isString(value: unknown): value is string { return typeof value === 'string';}// Check if value is numberfunction isNumber(value: unknown): value is number { return typeof value === 'number' && !isNaN(value);}// Check if value is arrayfunction isArray<T>(value: unknown): value is T[] { return Array.isArray(value);}// Complex object validationinterface 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' );}// Usagefunction processData(data: unknown) { if (isUser(data)) { // Type narrowed to User console.log(data.name, data.email); }}
Example: Generic type guards
// Generic array element guardfunction isArrayOf<T>( value: unknown, guard: (item: unknown) => item is T): value is T[] { return Array.isArray(value) && value.every(guard);}// Usage with primitivesconst data: unknown = [1, 2, 3];if (isArrayOf(data, isNumber)) { // Type narrowed to number[] data.forEach(n => console.log(n.toFixed(2)));}// Nullable type guardfunction 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 and 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 truefunction 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 typefunction 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 undefinedfunction 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 elementsfunction 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 interfacefunction 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 handlingasync 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 assertiontype 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 propertytype 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 toofunction 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 uniontype 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 typestype 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; }}
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 checksfunction 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 fieldsinterface 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 typeoffunction 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 existenceinterface 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 narrowingfunction 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 stringfunction 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 narrowingfunction compare(x: string | number, y: string | number) { if (x === y) { // Both are same type (string or number) console.log(x, y); }}// Discriminant equalitytype 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 patternfunction 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 guardsfunction 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 narrowingfunction 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 assignmentfunction 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 assignmentfunction 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 assignmentsfunction 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 detectionfunction 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 flowtype 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 operatorsfunction 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
Enable 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