Back to Blog
TypeScriptTypesneverAdvanced

TypeScript never Type Guide

Master the TypeScript never type. From exhaustive checks to impossible states to type narrowing.

B
Bootspring Team
Engineering
April 19, 2020
7 min read

The never type represents values that never occur. Here's how to use it effectively.

Understanding never#

1// never is the bottom type 2// No value can be assigned to never 3let x: never; 4 5// x = 1; // Error 6// x = 'string'; // Error 7// x = null; // Error 8// x = undefined; // Error 9 10// never is a subtype of all types 11const n: never = undefined as never; 12const str: string = n; // OK - never assignable to anything 13const num: number = n; // OK 14const bool: boolean = n; // OK 15 16// But nothing is assignable to never (except never) 17// const n2: never = 'hello'; // Error

Functions That Never Return#

1// Function that throws 2function throwError(message: string): never { 3 throw new Error(message); 4} 5 6// Function with infinite loop 7function infiniteLoop(): never { 8 while (true) { 9 // Never exits 10 } 11} 12 13// Comparison with void 14function logMessage(msg: string): void { 15 console.log(msg); 16 // Returns undefined implicitly 17} 18 19function fail(msg: string): never { 20 throw new Error(msg); 21 // Never returns anything 22} 23 24// Usage in call chains 25function processData(data: unknown): string { 26 if (typeof data !== 'string') { 27 throwError('Expected string'); // never type - code below unreachable 28 } 29 return data.toUpperCase(); // TypeScript knows data is string 30}

Exhaustive Checking#

1// Ensure all cases handled 2type Shape = 'circle' | 'square' | 'triangle'; 3 4function getArea(shape: Shape): number { 5 switch (shape) { 6 case 'circle': 7 return Math.PI * 10 ** 2; 8 case 'square': 9 return 10 * 10; 10 case 'triangle': 11 return (10 * 10) / 2; 12 default: 13 // If all cases handled, shape is never 14 const _exhaustive: never = shape; 15 return _exhaustive; 16 } 17} 18 19// Add new shape - TypeScript catches missing case 20type ShapeV2 = 'circle' | 'square' | 'triangle' | 'rectangle'; 21 22function getAreaV2(shape: ShapeV2): number { 23 switch (shape) { 24 case 'circle': 25 return Math.PI * 10 ** 2; 26 case 'square': 27 return 10 * 10; 28 case 'triangle': 29 return (10 * 10) / 2; 30 // Missing 'rectangle' case! 31 default: 32 const _exhaustive: never = shape; 33 // Error: Type 'string' is not assignable to type 'never' 34 return _exhaustive; 35 } 36} 37 38// Helper function 39function assertNever(value: never, message?: string): never { 40 throw new Error(message || `Unexpected value: ${value}`); 41} 42 43function getAreaWithHelper(shape: Shape): number { 44 switch (shape) { 45 case 'circle': 46 return Math.PI * 10 ** 2; 47 case 'square': 48 return 10 * 10; 49 case 'triangle': 50 return (10 * 10) / 2; 51 default: 52 return assertNever(shape, `Unknown shape: ${shape}`); 53 } 54}

Type Narrowing#

1// never appears after exhaustive narrowing 2function process(value: string | number) { 3 if (typeof value === 'string') { 4 console.log(value.toUpperCase()); 5 } else if (typeof value === 'number') { 6 console.log(value.toFixed(2)); 7 } else { 8 // value is never here - all cases handled 9 const _check: never = value; 10 } 11} 12 13// With discriminated unions 14type Result = 15 | { type: 'success'; data: string } 16 | { type: 'error'; error: Error }; 17 18function handleResult(result: Result) { 19 switch (result.type) { 20 case 'success': 21 console.log(result.data); 22 break; 23 case 'error': 24 console.error(result.error); 25 break; 26 default: 27 const _exhaustive: never = result; 28 } 29}

Conditional Types#

1// never in conditional types 2type NonNullable<T> = T extends null | undefined ? never : T; 3 4type A = NonNullable<string | null>; // string 5type B = NonNullable<undefined | number>; // number 6type C = NonNullable<null>; // never 7 8// Filter from union 9type Filter<T, U> = T extends U ? never : T; 10 11type Numbers = Filter<string | number | boolean, string>; 12// number | boolean 13 14// Exclude (built-in) 15type Exclude<T, U> = T extends U ? never : T; 16 17type WithoutString = Exclude<string | number | boolean, string>; 18// number | boolean 19 20// Extract (built-in) 21type Extract<T, U> = T extends U ? T : never; 22 23type OnlyStrings = Extract<string | number | boolean, string>; 24// string 25 26// never in unions disappears 27type Union = string | never | number; 28// string | number

Impossible States#

1// Represent impossible combinations 2type LoadingState = { 3 status: 'loading'; 4 data?: never; // Can't have data while loading 5 error?: never; // Can't have error while loading 6}; 7 8type SuccessState = { 9 status: 'success'; 10 data: string; 11 error?: never; // Can't have error on success 12}; 13 14type ErrorState = { 15 status: 'error'; 16 data?: never; // Can't have data on error 17 error: Error; 18}; 19 20type State = LoadingState | SuccessState | ErrorState; 21 22// TypeScript enforces valid states 23const loading: State = { status: 'loading' }; // OK 24const success: State = { status: 'success', data: 'Hello' }; // OK 25const error: State = { status: 'error', error: new Error() }; // OK 26 27// Invalid states caught at compile time 28// const invalid: State = { 29// status: 'success', 30// data: 'Hello', 31// error: new Error() // Error: Type 'Error' is not assignable to type 'undefined' 32// };

Function Overloads#

1// never helps with overload resolution 2function processValue(value: string): string; 3function processValue(value: number): number; 4function processValue(value: string | number): string | number { 5 if (typeof value === 'string') { 6 return value.toUpperCase(); 7 } 8 return value * 2; 9} 10 11// Without proper overloads, return would include never 12type Result = ReturnType<typeof processValue>; 13// string | number (never filtered out)

Generic Constraints#

1// Use never for impossible generics 2type Keys<T> = T extends object ? keyof T : never; 3 4type StringKeys = Keys<{ a: 1; b: 2 }>; // 'a' | 'b' 5type NoKeys = Keys<string>; // never 6 7// Require at least one property 8type RequireAtLeastOne<T> = { 9 [K in keyof T]: Pick<T, K>; 10}[keyof T]; 11 12// If no properties, becomes never 13type Empty = RequireAtLeastOne<{}>; // never 14 15// Mutual exclusion 16type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; 17 18type User = { 19 id: number; 20 name: string; 21 email: string; 22}; 23 24type UserWithoutId = Without<User, 'id'>; 25// { name: string; email: string; }

Error Handling Patterns#

1// Type-safe error handling 2type Success<T> = { success: true; value: T }; 3type Failure<E> = { success: false; error: E }; 4type Result<T, E = Error> = Success<T> | Failure<E>; 5 6function divide(a: number, b: number): Result<number, string> { 7 if (b === 0) { 8 return { success: false, error: 'Division by zero' }; 9 } 10 return { success: true, value: a / b }; 11} 12 13function handleResult<T, E>( 14 result: Result<T, E>, 15 onSuccess: (value: T) => void, 16 onFailure: (error: E) => void 17): void { 18 if (result.success) { 19 onSuccess(result.value); 20 } else { 21 onFailure(result.error); 22 } 23} 24 25// Never-returning error handler 26function panic(error: string): never { 27 console.error(error); 28 process.exit(1); 29} 30 31function unwrap<T>(result: Result<T>): T { 32 if (result.success) { 33 return result.value; 34 } 35 return panic(`Unwrap failed: ${result.error}`); 36}

Best Practices#

Use never For: ✓ Exhaustive switch checks ✓ Functions that throw/loop forever ✓ Filtering union types ✓ Representing impossible states Exhaustive Checking: ✓ Add default case with never ✓ Create assertNever helper ✓ Catch missing cases at compile time ✓ Use discriminated unions Type Design: ✓ Use never for impossible properties ✓ Filter with conditional types ✓ Validate completeness ✓ Document never in APIs Avoid: ✗ Assigning to never intentionally ✗ Ignoring never in results ✗ Over-complicating with never ✗ Forgetting exhaustive checks

Conclusion#

The never type represents impossibility in TypeScript's type system. Use it for exhaustive checks, functions that don't return, conditional type filtering, and representing impossible states. It's essential for type-safe switch statements and catching missing cases at compile time.

Share this article

Help spread the word about Bootspring