|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/never-unknown
$cat docs/typescript-—-never-&-unknown-types.md
updated Last week·14 min read·published

TypeScript — never & unknown Types

TypeScriptIntermediate🎯Free Tools
Introduction

TypeScript has two special types that sit at opposite ends of the type hierarchy. never represents values that can never occur — the bottom type. unknown represents any value — the top type that is type-safe. Understanding both is essential for writing robust, type-checked code.

The never Type

never is the type of values that never exist. Functions that always throw or have infinite loops return never. Variables of type never cannot be assigned any value — not even null or undefined.

never_type.ts
TypeScript
1// A function that always throws returns never
2function throwError(message: string): never {
3 throw new Error(message);
4}
5
6// A function with infinite loop returns never
7function infiniteLoop(): never {
8 while (true) {
9 // runs forever
10 }
11}
12
13// Exhaustive switch — never is used for type safety
14type Shape =
15 | { kind: "circle"; radius: number }
16 | { kind: "rectangle"; width: number; height: number }
17 | { kind: "triangle"; base: number; height: number };
18
19function area(shape: Shape): number {
20 switch (shape.kind) {
21 case "circle":
22 return Math.PI * shape.radius ** 2;
23 case "rectangle":
24 return shape.width * shape.height;
25 case "triangle":
26 return (shape.base * shape.height) / 2;
27 default:
28 const _exhaustive: never = shape;
29 return _exhaustive;
30 }
31}
32
33// If you add a new shape to the union and forget the case,
34// TypeScript will error: Type '"pentagon"' is not assignable to type 'never'
35
36// never can be assigned to any type (bottom type property)
37const str: string = throwError("oops"); // OK — never is assignable to string
38const num: number = throwError("oops"); // OK
39
40// never cannot be assigned FROM any type
41const neverVal: never = undefined as never;
42// const x: string = neverVal; // Error — never is not assignable to string
43// (unless strictNullChecks is off, where undefined IS assignable to string)
44
45// Void vs never
46function log(msg: string): void {
47 console.log(msg); // returns undefined implicitly
48}
49
50function crash(msg: string): never {
51 throw new Error(msg); // never returns
52
53 // A function that returns void CAN be used where never is expected
54 // if TypeScript allows implicit coercion. In practice, void means
55 // "the return value is not used", while never means "this never completes."
56}

info

Use the never exhaustiveness check pattern in switch statements. When you add a new union variant, TypeScript will immediately flag every unhandled case as a compile error.
Exhaustive Checks in Practice
exhaustive.ts
TypeScript
1// Real-world exhaustive discriminated union
2type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
3
4interface Route {
5 method: HttpMethod;
6 path: string;
7 handler: () => void;
8}
9
10function registerRoute(route: Route): void {
11 switch (route.method) {
12 case "GET":
13 console.log(`GET ${route.path}`);
14 break;
15 case "POST":
16 console.log(`POST ${route.path}`);
17 break;
18 case "PUT":
19 console.log(`PUT ${route.path}`);
20 break;
21 case "DELETE":
22 console.log(`DELETE ${route.path}`);
23 break;
24 case "PATCH":
25 console.log(`PATCH ${route.path}`);
26 break;
27 default:
28 const _exhaustive: never = route.method;
29 throw new Error(`Unknown method: ${_exhaustive}`);
30 }
31}
32
33// Adding "HEAD" to HttpMethod forces all switch statements to handle it
34// Type '"HEAD"' is not assignable to type 'never'
35
36// Exhaustive check as a utility function
37function assertNever(value: never): never {
38 throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
39}
40
41// Usage in function
42function getStatusColor(status: "success" | "error" | "warning" | "info"): string {
43 switch (status) {
44 case "success": return "#22C55E";
45 case "error": return "#EF4444";
46 case "warning": return "#F59E0B";
47 case "info": return "#3B82F6";
48 default: return assertNever(status);
49 }
50}
51
52// Async exhaustive check
53type EventKind = "created" | "updated" | "deleted" | "archived";
54
55async function handleEvent(event: { kind: EventKind; data: unknown }) {
56 switch (event.kind) {
57 case "created":
58 await createRecord(event.data);
59 break;
60 case "updated":
61 await updateRecord(event.data);
62 break;
63 case "deleted":
64 await deleteRecord(event.data);
65 break;
66 case "archived":
67 await archiveRecord(event.data);
68 break;
69 default:
70 assertNever(event.kind);
71 }
72}
The unknown Type

unknown is the type-safe counterpart to any. Like any, unknown can hold any value. Unlike any, you cannot use unknown without first narrowing its type.

unknown_type.ts
TypeScript
1// unknown — can hold any value
2let data: unknown = "hello";
3data = 42;
4data = { name: "Alice" };
5data = [1, 2, 3];
6
7// Cannot use unknown directly
8// const len: number = data.length; // Error: 'data' is of type 'unknown'
9
10// Must narrow before use
11if (typeof data === "string") {
12 console.log(data.length); // OK — narrowed to string
13}
14
15if (Array.isArray(data)) {
16 console.log(data.length); // OK — narrowed to any[]
17}
18
19// Type assertion as a last resort
20const str = data as string; // No compile error, but no runtime check
21
22// Safe type assertion with runtime check
23function asString(value: unknown): string {
24 if (typeof value === "string") return value;
25 throw new TypeError(`Expected string, got ${typeof value}`);
26}
27
28// JSON.parse returns any — wrap it with unknown
29function safeJsonParse<T>(json: string): unknown {
30 return JSON.parse(json);
31}
32
33const parsed = safeJsonParse('{"name":"Alice"}');
34// parsed is unknown — must narrow
35if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
36 console.log((parsed as { name: string }).name);
37}
38
39// unknown in function parameters — forces caller to be explicit
40function processValue(value: unknown): string {
41 if (value === null || value === undefined) return "";
42 if (typeof value === "string") return value;
43 if (typeof value === "number") return String(value);
44 if (typeof value === "boolean") return value ? "true" : "false";
45 if (Array.isArray(value)) return value.map(String).join(", ");
46 return JSON.stringify(value);
47}
48
49// API response typed as unknown
50async function fetchJson<T>(url: string): Promise<T> {
51 const response = await fetch(url);
52 const data: unknown = await response.json();
53 // Validate data matches T before returning
54 return data as T;
55}

best practice

Replace any with unknown whenever possible. unknown forces you to validate data at the boundary, preventing runtime errors deep in your code.
Narrowing Techniques
narrowing.ts
TypeScript
1// Type narrowing — TypeScript eliminates branches as impossible
2
3// 1. typeof narrowing
4function format(value: unknown): string {
5 if (typeof value === "string") return value.toUpperCase();
6 if (typeof value === "number") return value.toFixed(2);
7 if (typeof value === "boolean") return value ? "yes" : "no";
8 if (typeof value === "undefined") return "N/A";
9 return String(value);
10}
11
12// 2. instanceof narrowing
13function handleError(error: unknown): string {
14 if (error instanceof TypeError) return `Type: ${error.message}`;
15 if (error instanceof RangeError) return `Range: ${error.message}`;
16 if (error instanceof Error) return `Error: ${error.message}`;
17 return "Unknown error";
18}
19
20// 3. Truthiness narrowing
21function process(input: string | null | undefined): string {
22 if (!input) return "empty";
23 return input.toUpperCase(); // narrowed to string
24}
25
26// 4. Equality narrowing
27function compare(a: string | number, b: string | boolean) {
28 if (a === b) {
29 // a and b are both string (the only common type)
30 return a.toUpperCase();
31 }
32}
33
34// 5. in narrowing
35interface Fish { swim(): void; }
36interface Bird { fly(): void; }
37
38function move(animal: Fish | Bird) {
39 if ("swim" in animal) {
40 animal.swim(); // narrowed to Fish
41 } else {
42 animal.fly(); // narrowed to Bird
43 }
44}
45
46// 6. Discriminated union narrowing
47type Result<T> =
48 | { success: true; data: T }
49 | { success: false; error: string };
50
51function handleResult(result: Result<string>) {
52 if (result.success) {
53 console.log(result.data.toUpperCase()); // narrowed to { success: true; data: string }
54 } else {
55 console.error(result.error); // narrowed to { success: false; error: string }
56 }
57}
58
59// 7. Assertion function narrowing
60function assertString(value: unknown): asserts value is string {
61 if (typeof value !== "string") {
62 throw new TypeError(`Expected string, got ${typeof value}`);
63 }
64}
65
66const val: unknown = "hello";
67assertString(val);
68console.log(val.length); // narrowed to string — no cast needed
Type Hierarchy
hierarchy.ts
TypeScript
1// TypeScript type hierarchy (top to bottom):
2
3// unknown — the top type (accepts everything)
4// any — escape hatch (bypasses type checking)
5// object — non-primitive types
6// {} — any non-null/undefined value
7// string | number | boolean | symbol | bigint | null | undefined
8// never — the bottom type (accepts nothing)
9
10// Assignability (top → bottom):
11let x: unknown;
12x = 42; // OK — unknown accepts anything
13x = "hello"; // OK
14x = null; // OK
15
16let y: any;
17y = 42; // OK — any accepts anything
18y = "hello"; // OK
19y = null; // OK
20
21// never is assignable to everything
22declare const n: never;
23const a: string = n; // OK
24const b: number = n; // OK
25const c: object = n; // OK
26const d: unknown = n; // OK
27
28// But nothing is assignable to never (except never itself)
29// const e: never = 42; // Error
30
31// Practical comparison
32type SafeParse = (input: unknown) => Result<string>;
33type UnsafeParse = (input: any) => Result<string>;
34type CrashParse = (input: never) => Result<string>;
35// CrashParse can never be called — input can never be provided
36
37// never in conditional types
38type NonNullable<T> = T extends null | undefined ? never : T;
39type A = NonNullable<string | null | undefined>; // string
40
41// never in mapped types — filter out keys
42type FilterNever<T> = {
43 [K in keyof T as T[K] extends never ? never : K]: T[K];
44};
45
46type Mixed = { a: string; b: never; c: number; d: undefined };
47type Cleaned = FilterNever<Mixed>; // { a: string; c: number; d: undefined }
48
49// The any vs unknown decision
50function processAny(value: any) {
51 // No type checking — dangerous
52 return value.foo.bar.baz;
53}
54
55function processUnknown(value: unknown) {
56 // Must narrow — safe
57 if (
58 typeof value === "object" &&
59 value !== null &&
60 "foo" in value
61 ) {
62 const obj = value as { foo: { bar: { baz: string } } };
63 return obj.foo.bar.baz;
64 }
65 throw new TypeError("Invalid value");
66}

info

Always prefer unknown over any for function parameters and API responses. Reserve any for third-party library integration or migration scenarios.
Best Practices

1. Use never exhaustiveness checks in every switch statement over a discriminated union.

2. Replace any with unknown for function parameters, especially at API boundaries.

3. Create asserts value is T helper functions for runtime type validation.

4. Use never as the return type of functions that always throw or loop infinitely.

5. Use never in conditional types to filter out unwanted union members.

6. When working with JSON.parse, wrap the result as unknown and validate before use.

$Blueprint — Engineering Documentation·Section ID: TS-NEVER·Revision: 1.0