Never, Unknown & Void Types
10 questions found
What does the 'void' type mean for a function's return type, and how does it differ from explicitly returning undefined?
Beginner
'void' indicates a function's return value should be IGNORED by callers, even if the function actually returns undefined (or nothing at all) at runtime — it's specifically about the CALLER's perspective and intent, distinct from 'undefined' which is about the actual returned value's type.
function logMessage(message: string): void {
console.log(message);
// implicitly returns undefined, but callers shouldn't rely on or use that
}
const result = logMessage('hi'); // result's type is void; using it meaningfully is discouraged
Real-world example
Typing an event handler or callback whose return value is never meant to be used, like a click handler.
Common follow-ups: Can a function typed to return 'void' actually be assigned an implementation that returns a real value, like true?
Function Types
Overloads & Optional/Default Parameters
What does the 'unknown' type represent, and how is it safer than 'any'?
Beginner
'unknown' represents a value whose type isn't known yet — like 'any', it can hold ANY value, but unlike 'any', you CANNOT perform any operations on an 'unknown' value (property access, calling it, arithmetic) until you first narrow it to a more specific type via a type guard, ensuring you can't accidentally misuse it.
function processInput(input: unknown) {
// input.toUpperCase(); // Error: 'input' is of type 'unknown'
if (typeof input === 'string') {
console.log(input.toUpperCase()); // OK, narrowed to string
}
}
Real-world example
Typing a value from an untrusted source (JSON.parse result, external API response) as 'unknown' to force safe narrowing before use.
Common follow-ups: Why is 'unknown' generally recommended over 'any' for function parameters accepting external/uncertain data?
Type Narrowing & Guards
What does the 'never' type represent, and when does a function's return type become 'never'?
Beginner
'never' represents a value that can NEVER actually occur — a function's return type is inferred as 'never' when it always throws an exception, always enters an infinite loop, or otherwise never actually completes and returns control to the caller.
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
Real-world example
Typing a utility function that always throws (like an assertion helper) so the compiler knows code after calling it is unreachable.
Common follow-ups: How is 'never' different from 'void', given both seem to relate to functions that 'don't return anything useful'?
Function Types
Overloads & Optional/Default Parameters
Why is 'never' the correct type for an empty array literal used as a union's building block, and how does it behave as a 'bottom type'?
Intermediate
'never' is TypeScript's bottom type — a subtype of EVERY other type, but no type (except never itself) is a subtype of it. This means never is compatible everywhere it's expected, and in a union type, `T | never` simplifies to just `T`, since never contributes no actual possible values — this is exactly why filtering unions down to nothing via conditional types (like Exclude) naturally lands on never.
type A = string | never; // simplifies to just 'string'
type B = never | number | never; // simplifies to just 'number'
Real-world example
Understanding why Exclude<T,U> and other conditional-type utilities rely on never as their 'this branch produces nothing' signal.
Common follow-ups: Why can you assign a 'never' value to a variable of ANY other type, but not the reverse?
Conditional Types
How does 'unknown' interact with union types, and why does `unknown | T` always simplify to just 'unknown'?
Intermediate
Since 'unknown' is TypeScript's top type — every other type is assignable TO it — combining it with anything in a union absorbs that other type entirely, because 'unknown' already represents 'could be absolutely anything', making any additional union member redundant information.
type A = unknown | string; // simplifies to just 'unknown'
type B = string | unknown | number; // simplifies to just 'unknown'
Real-world example
Understanding why accidentally including 'unknown' in a union type silently widens the whole type, discarding your more specific members.
Common follow-ups: How does 'any' behave differently from 'unknown' when combined into a union this way?
Union & Intersection Types
Why does assigning a function that returns a specific value (like `() => string`) to a variable typed as `() => void` compile without error?
Intermediate
TypeScript special-cases 'void' function-type COMPATIBILITY: a function returning any actual value CAN be assigned where a void-returning function is expected, because the caller has explicitly signaled it will ignore whatever comes back — this allows patterns like passing Array.prototype.push (which returns a number) as a void-expecting callback without extra wrapping.
const callback: () => void = () => 'some value'; // allowed, even though it returns a string
const arr: number[] = [];
const pushWrapper: () => void = () => arr.push(42); // 'push' returns a number, still OK here
Real-world example
Passing existing functions with meaningful return values (like array mutator methods) directly as void-expecting callbacks without a wrapper.
Common follow-ups: Does this same leniency apply to a variable EXPLICITLY typed as 'undefined' instead of 'void'?
Function Types
Overloads & Optional/Default Parameters
How would you write a custom type guard that safely narrows an 'unknown' value into a specific, validated interface shape?
Advanced
Write a function with a `value is InterfaceName` return type predicate, performing thorough runtime checks (typeof, property existence, nested shape validation) against the unknown value — this bridges the gap between untyped external data and your application's actual typed domain models.
interface User { id: string; name: string; }
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value && typeof (value as any).id === 'string' &&
'name' in value && typeof (value as any).name === 'string'
);
}
function handleResponse(data: unknown) {
if (isUser(data)) console.log(data.name); // safely narrowed to User
}
Real-world example
Validating an untrusted API response's actual runtime shape before trusting it as a specific typed domain object.
Common follow-ups: How do runtime validation libraries like Zod or io-ts relate to this manual type-guard pattern, and what do they add?
Type Narrowing & Guards
How does the compiler use 'never' to achieve exhaustiveness checking in a switch statement over a union type?
Advanced
After a switch statement has handled every known member of a union (via case blocks for each discriminant value), the TYPE of the variable in a trailing default case narrows down to 'never' — because every possible value has already been accounted for, leaving logically zero remaining possibilities — and assigning that never-typed value to a never-typed parameter (like assertNever) triggers a compile error only if a case was actually missed.
type Shape = { kind: 'circle' } | { kind: 'square' };
function handle(shape: Shape) {
switch (shape.kind) {
case 'circle': return 'round';
case 'square': return 'angular';
default:
const exhaustiveCheck: never = shape; // errors only if a case is missing
throw new Error(`Unhandled: ${exhaustiveCheck}`);
}
}
Real-world example
Building a compile-time safety net that fails the build if a new Shape variant is added without updating every relevant switch statement.
Common follow-ups: What specific compiler error message appears if you add a new union variant and forget to handle it here?
Discriminated Unions & Exhaustiveness Checking
Why does an intersection of two 'incompatible' primitive types, like `string & number`, resolve to 'never'?
Advanced
An intersection type requires a value to satisfy ALL constituent types SIMULTANEOUSLY — since no value can be both a string AND a number at the same time (they're fundamentally incompatible primitive shapes), the compiler correctly determines that no such value could ever exist, so the intersection resolves to the 'impossible value' type, never.
type Impossible = string & number; // never
// let x: Impossible = 'hello'; // Error: Type 'string' is not assignable to type 'never'
Real-world example
Understanding a confusing 'never' type appearing unexpectedly after intersecting two incompatible generic type parameters.
Common follow-ups: Under what more realistic circumstances (like generic constraints) might you accidentally produce a `T & U` intersection that resolves to never?
Union & Intersection Types
How would you write a generic 'assertIsDefined' utility using 'never' and TypeScript's assertion function syntax to narrow away null/undefined?
Advanced
Declare a function with an 'asserts value is T' return type (a TypeScript assertion function) that throws (returning 'never' internally) if the value is null or undefined — after calling it, TypeScript automatically narrows the checked variable's type at every subsequent line in that scope, without needing an explicit if-check wrapping the rest of the code.
function assertIsDefined<T>(value: T, message = 'Value is not defined'): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(message);
}
}
function process(user: User | undefined) {
assertIsDefined(user);
console.log(user.name); // narrowed to User, no more '| undefined', for the rest of this scope
}
Real-world example
Removing repetitive null-checking boilerplate in functions that need to guarantee a value is defined before proceeding, common in test helpers and validation layers.
Common follow-ups: How does an 'asserts' function's narrowing effect differ from a regular 'value is T' type predicate function's effect?
Nullability: strictNullChecks
Optional Chaining & Nullish Coalescing