Back to Blog
TypeScriptNeverUnknownTypes

TypeScript never and unknown Types

Master TypeScript never and unknown types for type-safe programming.

B
Bootspring Team
Engineering
October 11, 2018
7 min read

The never and unknown types are essential for type-safe programming in TypeScript. Here's how to use them.

The unknown Type#

1// unknown is the type-safe version of any 2let value: unknown; 3 4value = 'hello'; 5value = 42; 6value = { name: 'John' }; 7value = [1, 2, 3]; 8 9// Can't use unknown directly 10const str: unknown = 'hello'; 11// str.toUpperCase(); // Error: Object is of type 'unknown' 12 13// Must narrow the type first 14if (typeof str === 'string') { 15 str.toUpperCase(); // OK 16} 17 18// unknown in function parameters 19function processValue(value: unknown) { 20 if (typeof value === 'string') { 21 return value.toUpperCase(); 22 } 23 if (typeof value === 'number') { 24 return value * 2; 25 } 26 return String(value); 27}

unknown vs any#

1// any - bypasses type checking (unsafe) 2function withAny(value: any) { 3 value.foo(); // No error, might crash at runtime 4 value.bar.baz(); // No error 5 const num: number = value; // No error 6} 7 8// unknown - requires type checking (safe) 9function withUnknown(value: unknown) { 10 // value.foo(); // Error: Object is of type 'unknown' 11 12 // Must narrow first 13 if (value && typeof value === 'object' && 'foo' in value) { 14 (value as { foo: () => void }).foo(); // OK 15 } 16} 17 18// Use unknown for truly unknown values 19// Use any only for migration/interop

Type Narrowing with unknown#

1// typeof narrowing 2function processUnknown(value: unknown): string { 3 if (typeof value === 'string') { 4 return value; 5 } 6 if (typeof value === 'number') { 7 return value.toString(); 8 } 9 if (typeof value === 'boolean') { 10 return value ? 'yes' : 'no'; 11 } 12 return 'unknown'; 13} 14 15// instanceof narrowing 16function processError(error: unknown): string { 17 if (error instanceof Error) { 18 return error.message; 19 } 20 if (typeof error === 'string') { 21 return error; 22 } 23 return 'Unknown error'; 24} 25 26// Type predicate 27function isUser(value: unknown): value is User { 28 return ( 29 typeof value === 'object' && 30 value !== null && 31 'id' in value && 32 'name' in value 33 ); 34} 35 36function processUser(data: unknown) { 37 if (isUser(data)) { 38 console.log(data.name); // OK, data is User 39 } 40}

The never Type#

1// never represents values that never occur 2 3// Function that never returns 4function throwError(message: string): never { 5 throw new Error(message); 6} 7 8// Infinite loop 9function infiniteLoop(): never { 10 while (true) { 11 // do something 12 } 13} 14 15// never in union types 16type NonNull<T> = T extends null | undefined ? never : T; 17 18type StringOrNull = string | null; 19type StringOnly = NonNull<StringOrNull>; // string

Exhaustive Checks#

1// never for exhaustive checking 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 // shape is never here - all cases handled 14 const exhaustiveCheck: never = shape; 15 throw new Error(`Unknown shape: ${exhaustiveCheck}`); 16 } 17} 18 19// If you add a new shape type without handling it: 20type Shape2 = 'circle' | 'square' | 'triangle' | 'rectangle'; 21 22function getArea2(shape: Shape2): 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 default: 31 // Error: Type 'string' is not assignable to type 'never' 32 const exhaustiveCheck: never = shape; 33 throw new Error(`Unknown shape: ${exhaustiveCheck}`); 34 } 35} 36 37// Helper function 38function assertNever(value: never): never { 39 throw new Error(`Unexpected value: ${value}`); 40}

never in Conditional Types#

1// Exclude from union 2type Exclude<T, U> = T extends U ? never : T; 3 4type Numbers = 1 | 2 | 3 | 4 | 5; 5type SmallNumbers = Exclude<Numbers, 4 | 5>; // 1 | 2 | 3 6 7// Extract from union 8type Extract<T, U> = T extends U ? T : never; 9 10type ExtractedNumbers = Extract<Numbers, 2 | 3 | 4>; // 2 | 3 | 4 11 12// Filter object properties 13type FilterByType<T, U> = { 14 [K in keyof T as T[K] extends U ? K : never]: T[K] 15}; 16 17interface Mixed { 18 name: string; 19 age: number; 20 email: string; 21 active: boolean; 22} 23 24type StringProps = FilterByType<Mixed, string>; 25// { name: string; email: string; }

unknown in API Responses#

1// Safe API response handling 2async function fetchData<T>( 3 url: string, 4 validator: (data: unknown) => data is T 5): Promise<T> { 6 const response = await fetch(url); 7 const data: unknown = await response.json(); 8 9 if (validator(data)) { 10 return data; 11 } 12 13 throw new Error('Invalid response data'); 14} 15 16// Validator 17interface User { 18 id: number; 19 name: string; 20} 21 22function isUser(data: unknown): data is User { 23 return ( 24 typeof data === 'object' && 25 data !== null && 26 typeof (data as User).id === 'number' && 27 typeof (data as User).name === 'string' 28 ); 29} 30 31// Usage 32const user = await fetchData<User>('/api/user', isUser); 33console.log(user.name); // Type-safe

Error Handling with unknown#

1// In catch blocks, error is unknown (TypeScript 4.4+) 2async function safeOperation() { 3 try { 4 await riskyOperation(); 5 } catch (error: unknown) { 6 // Must narrow the error type 7 if (error instanceof Error) { 8 console.error(error.message); 9 } else if (typeof error === 'string') { 10 console.error(error); 11 } else { 12 console.error('Unknown error', error); 13 } 14 } 15} 16 17// Helper function 18function getErrorMessage(error: unknown): string { 19 if (error instanceof Error) return error.message; 20 if (typeof error === 'string') return error; 21 return 'Unknown error'; 22} 23 24// Usage 25try { 26 throw 'Something went wrong'; 27} catch (error: unknown) { 28 console.error(getErrorMessage(error)); 29}

never as Return Type#

1// Functions that throw 2function fail(message: string): never { 3 throw new Error(message); 4} 5 6// Using with conditional logic 7function processValue(value: string | number): string { 8 if (typeof value === 'string') { 9 return value; 10 } 11 if (typeof value === 'number') { 12 return value.toString(); 13 } 14 // TypeScript knows this is unreachable 15 return fail('Unexpected value type'); 16} 17 18// Assertion functions 19function assert(condition: boolean, msg?: string): asserts condition { 20 if (!condition) { 21 throw new Error(msg ?? 'Assertion failed'); 22 } 23} 24 25function processUser(user: User | null) { 26 assert(user !== null, 'User is required'); 27 // user is User here, not null 28 console.log(user.name); 29}

Type Inference with never and unknown#

1// Union with never 2type A = string | never; // string (never disappears) 3 4// Intersection with never 5type B = string & never; // never (never absorbs) 6 7// Union with unknown 8type C = string | unknown; // unknown (unknown absorbs) 9 10// Intersection with unknown 11type D = string & unknown; // string (unknown disappears) 12 13// Practical implications 14type NonNullable<T> = T extends null | undefined ? never : T; 15 16type E = NonNullable<string | null>; // string 17type F = NonNullable<undefined>; // never

never in Mapped Types#

1// Remove keys from type 2type RemoveKeys<T, K extends keyof T> = { 3 [P in keyof T as P extends K ? never : P]: T[P] 4}; 5 6interface User { 7 id: number; 8 name: string; 9 password: string; 10 email: string; 11} 12 13type SafeUser = RemoveKeys<User, 'password'>; 14// { id: number; name: string; email: string; } 15 16// Filter keys by type 17type KeysOfType<T, V> = { 18 [K in keyof T]: T[K] extends V ? K : never 19}[keyof T]; 20 21type StringKeys = KeysOfType<User, string>; // 'name' | 'password' | 'email'

Best Practices#

Use unknown When: ✓ Receiving external data (API, user input) ✓ Catch blocks ✓ JSON parsing ✓ Generic value containers Use never When: ✓ Functions that throw or loop forever ✓ Exhaustive type checking ✓ Filtering union types ✓ Impossible code paths Narrowing unknown: ✓ typeof for primitives ✓ instanceof for classes ✓ Type predicates for objects ✓ Assertion functions Avoid: ✗ Using any instead of unknown ✗ Type assertions without validation ✗ Ignoring never in switch defaults ✗ Over-complicating type guards

Conclusion#

unknown is the type-safe alternative to any - use it for values whose type is truly unknown and narrow with type guards. never represents impossible values - use it for exhaustive checks, functions that never return, and filtering types. Together, they enable robust type-safe programming while maintaining flexibility for dynamic values.

Share this article

Help spread the word about Bootspring