Difficulty: Beginner
Explain the difference between unknown, any, and never in TypeScript. When should you use each?
These three types represent the extremes of TypeScript's type system:
'any' is the escape hatch - it disables ALL type checking. Any value can be assigned to 'any', and 'any' can be assigned to anything. Use it only for migration or truly dynamic scenarios.
'unknown' is the type-safe counterpart to 'any'. Any value can be assigned to 'unknown', but you MUST narrow it before using it. It forces you to check the type before performing operations.
'never' is the bottom type - it represents values that never occur. No value can be assigned to 'never'. It appears in exhaustive checks, functions that never return, and impossible intersections.
Type hierarchy: any/unknown (top) > all types > never (bottom).
// any - disables type checking entirely
let risky: any = 'hello';
risky.foo.bar.baz(); // No error! Crashes at runtime
risky = 42;
risky = true;
const num: number = risky; // OK, no checking
// unknown - type-safe alternative
let safe: unknown = 'hello';
// safe.toUpperCase(); // Error: Object is of type 'unknown'
// const num: number = safe; // Error: cannot assign unknown to number
// Must narrow before use
if (typeof safe === 'string') {
safe.toUpperCase(); // OK after narrowing
}
// Practical: parsing JSON safely
function parseJSON(json: string): unknown {
return JSON.parse(json); // returns unknown, not any
}
const data = parseJSON('{"name": "Alice"}');
// data.name; // Error - must narrow first
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name); // OK
}
Use 'unknown' instead of 'any' for values from external sources (APIs, JSON, user input). It forces safe handling without disabling type checking.
// Functions that never return
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
// Exhaustive checking
type Animal = 'cat' | 'dog' | 'bird';
function sound(animal: Animal): string {
switch (animal) {
case 'cat': return 'meow';
case 'dog': return 'woof';
case 'bird': return 'chirp';
default:
// If someone adds 'fish' to Animal and forgets to handle it:
const exhaustive: never = animal;
// Error: Type 'fish' is not assignable to type 'never'
return exhaustive;
}
}
// Impossible types
type StringAndNumber = string & number; // never
type EmptyUnion = never; // no possible values
// never in conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | null | undefined>;
// type: string (null and undefined filtered out)
never represents impossibility. It's essential for exhaustive checks (catching unhandled union members) and filtering types in conditional types.
// Type assignability
let a: any;
let u: unknown;
let n: never;
// Assigning TO these types:
a = 42; // OK - any accepts everything
u = 42; // OK - unknown accepts everything
// n = 42; // Error - nothing assignable to never
// Assigning FROM these types:
let s1: string = a; // OK - any assignable to anything
// let s2: string = u; // Error - unknown not assignable
// let s3: string = n; // OK technically (never assignable to anything)
// Real-world rule of thumb:
// - External data (API, JSON, user input): use unknown
// - Legacy code migration: any (temporarily)
// - Functions that throw/loop forever: never
// - Exhaustive switch defaults: never
// Error handling with unknown
try {
riskyOperation();
} catch (error: unknown) { // TS 4.4+ default
if (error instanceof Error) {
console.log(error.message);
} else {
console.log('Unknown error:', String(error));
}
}
In modern TypeScript, catch clauses default to 'unknown'. This forces you to check the error type before accessing properties, preventing runtime crashes.
unknown, any, never, Type Safety, Top and Bottom Types