Mastering TypeScript’s never, unknown, and any: When and Why to Use Them
This article explains how TypeScript’s never, unknown, and any types work, compares their roles as top and bottom types, shows practical code examples for safe type narrowing, and demonstrates advanced scenarios such as unreachable‑code detection, type operations, exhaustive checks, and proper use of never in real‑world projects.
1. Introduction
TypeScript introduced the never and unknown basic types in versions 2.0 and 3.0 respectively, greatly improving the type system. Many developers still rely on the old any approach from the early days of JavaScript, which can lead to bloated and unsafe code.
2. Top type and bottom type in TypeScript
In type‑system design there are two special kinds of types:
Top type – a universal supertype that can contain every possible value.
Bottom type – a type that represents no value; it is a subtype of every other type.
In TypeScript 3.0 there are two top types ( any and unknown) and one bottom type ( never).
3. unknown and any
3.1 unknown – the safe any
Although unknown is rarely seen in existing code, it is the type‑safe counterpart of any. The difference is illustrated by the following example:
function format1(value: any) {
value.toFixed(2); // not flagged, dangerous
}
function format2(value: unknown) {
value.toFixed(2); // error, red underline
// need to narrow the type
// 1. type assertion
(value as Number).toFixed(2);
// 2. type guard
if (typeof value === 'number') {
// inferred as number
value.toFixed(2);
}
// 3. assertion function
assertIsNumber(value);
value.toFixed(2);
}
/** type assertion function, throws error */
function assertIsNumber(arg: unknown): asserts arg is Number {
if (!(arg instanceof Number)) {
throw new TypeError('Not a Number: ' + arg);
}
}Using any is like exploring a haunted house – the code runs but type errors are hidden. unknown combined with type guards ensures safety even when upstream data is uncertain.
3.2 any – no type checking
When any is used, type checking is completely abandoned. Before using it, consider whether a more specific type is possible or whether unknown can replace it. Only when neither is feasible should any be the last resort.
3.3 Revisiting older type designs
Some existing type definitions still rely on any and could be improved:
3.3.1 String()
String()accepts any argument and converts it to a string. With unknown the signature could be safer, but any was used originally because unknown did not exist yet.
interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}3.3.2 JSON.parse()
The signature of JSON.parse was defined before unknown existed, so its return type is any:
interface JSON {
parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
...
}If unknown were used, the return type would be less precise, because the function was added to TypeScript before unknown was introduced.
4. never
The never type represents a value that never exists. It appears in two situations:
A function always throws an exception, so it never returns a value.
A function contains an infinite loop, making the return point unreachable.
function err(msg: string): never { // OK
throw new Error(msg);
}
function loopForever(): never { // OK
while (true) {}
}4.1 The unique bottom type
Because never is the only bottom type in TypeScript, it can be assigned to any other type:
let err: never;
let num: number = 4;
num = err; // OKIn set theory, unknown is the universal set, while never is the empty set; every type contains never.
4.1.1 null/undefined and never
Although null and undefined can be assigned to never, they cannot be assigned from never. Only never can be assigned to never itself.
// null and undefined can be assigned from never
declare const n: never;
let a: null = n; // OK
let b: undefined = n; // OK
let ne: never;
ne = null; // error
ne = undefined; // error
declare const an: any;
ne = an; // error
declare const nev: never;
ne = nev; // OK4.1.2 Why any is not a strict bottom type
Unlike never, any can be assigned to other types, so it does not satisfy the definition of a bottom type.
const a = 'anything';
const b: any = a; // OK
const c: never = a; // error4.2 Practical uses of never
Unreachable‑code checking – the compiler flags code after a never function.
Type operations – never acts as the minimal factor in unions and intersections.
Exhaustive checks – using never in a default branch forces handling of all union members.
…
Example of unreachable‑code detection:
process.exit(0);
console.log('hello world'); // Unreachable code detected.ts(7027)Another example with a listening loop:
function listen(): never {
while (true) {
let conn = server.accept();
}
}
listen();
console.log('!!!'); // Unreachable code detected.ts(7027)Marking a function as returning never helps the compiler narrow types after a throw:
function throwError(): never {
throw new Error();
}
function firstChar(msg: string | undefined) {
if (msg === undefined) throwError();
let chr = msg.charAt(1); // OK, msg is now string
}4.2.1 Type operations
Because never is the empty set, it satisfies the following algebraic rules:
T | never => T
T & never => neverThese rules simplify type calculations, for example when using Promise.race with a timeout that returns Promise<never>:
async function fetchNameWithTimeout(userId: string): Promise<string> {
const data = await Promise.race([
fetchData(userId),
timeout(3000)
]);
return data.userName;
}
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout!')), ms);
});
}If the timeout returned any or unknown, the resulting type would be any or unknown, losing type safety.
4.2.2 Conditional types
neveris often used in conditional types to represent the “else” branch:
type Arguments<T> = T extends (...args: infer A) => any ? A : never;
type Return<T> = T extends (...args: any[]) => infer R ? R : never;When a non‑function type is supplied, the result is never, producing a compile‑time error.
4.2.3 Exhaustive checks
For discriminated unions, a default case that assigns the value to a never variable forces the compiler to verify that all possible variants are handled.
interface Foo { type: 'foo' }
interface Bar { type: 'bar' }
type All = Foo | Bar;
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// val is Foo
break;
case 'bar':
// val is Bar
break;
default:
const exhaustiveCheck: never = val; // ensures exhaustiveness
break;
}
}If a new variant Baz is added to All, the assignment to never triggers a compile error, reminding the developer to handle the new case.
5. Conclusion
For developers who care about type safety and code design, TypeScript is a pragmatic language rather than a constraint. Understanding the roles of never, unknown, and any deepens knowledge of type‑system theory and set theory, and enables writing reliable, well‑narrowed code in real projects.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
