Special types
In this chapter, you learn about TypeScript's special types.
What you learn in this chapter
- What the any type is and why you should avoid it
- Safe type checking with the unknown type
- The difference between the void and never types and when to use them
- How to handle null and undefined
The content of this chapter is important for improving type safety. In particular, you learn the dangers of the any type and a safe alternative using the unknown type.
The any type (a type that disables type checking)
The any type is a special type that accepts a value of any type. It disables TypeScript's type checking.
// any type: you can assign any value
let value: any = 10;
value = 'Hello'; // OK
value = true; // OK
value = [1, 2, 3]; // OK
value = { name: 'Alice' }; // OK
// Any operation is allowed on an any variable
let anyValue: any = 'Hello';
anyValue.toUpperCase(); // OK (works at runtime)
anyValue.nonExistentMethod(); // no compile error (runtime error)
The problem with the any type
When you use the any type, TypeScript's type checking is disabled, and you miss bugs.
let userData: any = { name: 'Alice', age: 25 };
// Accessing a non-existent property is not an error
console.log(userData.email); // undefined (no compile error)
// You cannot notice type mistakes
let age: number = userData.age; // OK
let wrongAge: number = userData.name; // OK (compiles, but a problem at runtime)
Implicit any
When there is no type annotation and the type cannot be inferred, it becomes implicit any.
// With no type annotation and no initial value, it becomes implicit any
let implicitAny; // the type is any
implicitAny = 10;
implicitAny = 'Hello';
// Function parameters without type annotations also become any
function add(a, b) { // a and b are of type any
return a + b;
}
add(5, 10); // OK
add('5', '10'); // OK (possibly unintended behavior)
The noImplicitAny option
Setting noImplicitAny: true in tsconfig.json forbids implicit any.
// tsconfig.json
{
"compilerOptions": {
"noImplicitAny": true
}
}
// With noImplicitAny: true
function add(a, b) { // Error: Parameter 'a' implicitly has an 'any' type.
return a + b;
}
// Fix: add type annotations
function addFixed(a: number, b: number): number {
return a + b;
}
When you should use the any type (keep it to a minimum)
// 1. When an external library has no type definitions
let externalLib: any = require('some-old-library');
// 2. When doing gradual typing (use any temporarily, then change to a precise type later)
let temporaryAny: any = getData();
// Make the type clear later
let user: User = temporaryAny as User;
The any type undermines TypeScript's type safety. Avoid using it as much as possible, and use the unknown type described below.
The unknown type (a type-safe any)
The unknown type is similar to the any type but safer. You can assign any value, but you need a type check before using it.
// unknown type: you can assign any value
let value: unknown = 10;
value = 'Hello'; // OK
value = true; // OK
value = [1, 2, 3]; // OK
The difference from any
// any type: usable without type checking (dangerous)
let anyValue: any = 'Hello';
let str1: string = anyValue; // OK (no check)
str1.toUpperCase(); // OK
// unknown type: a type check is required (safe)
let unknownValue: unknown = 'Hello';
// let str2: string = unknownValue; // Error: a type check is required
// Check the type with a type guard before using it
if (typeof unknownValue === 'string') {
let str2: string = unknownValue; // OK (the type is determined)
str2.toUpperCase(); // OK
}
Patterns for using the unknown type
// Pattern 1: a type check with typeof
function processValue(value: unknown): void {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // string methods are available
} else if (typeof value === 'number') {
console.log(value.toFixed(2)); // number methods are available
} else if (typeof value === 'boolean') {
console.log(value ? 'Yes' : 'No');
}
}
processValue('hello'); // 'HELLO'
processValue(3.14159); // '3.14'
processValue(true); // 'Yes'
// Pattern 2: a type check with instanceof
function processDate(value: unknown): void {
if (value instanceof Date) {
console.log(value.toISOString()); // Date methods are available
}
}
// Pattern 3: a type check with the in operator
function processUser(value: unknown): void {
if (
typeof value === 'object' &&
value !== null &&
'name' in value
) {
console.log((value as { name: string }).name);
}
}
A practical example
// A function that processes a response from an API
function processApiResponse(response: unknown): void {
// Check the type safely with a type guard
if (
typeof response === 'object' &&
response !== null &&
'data' in response
) {
console.log((response as { data: unknown }).data);
} else {
console.error('Invalid response format');
}
}
// Parsing JSON
function parseJSON(jsonString: string): unknown {
try {
return JSON.parse(jsonString); // returns the unknown type
} catch (error) {
return null;
}
}
let data: unknown = parseJSON('{"name": "Alice"}');
// Check the type before using it
if (data && typeof data === 'object' && 'name' in data) {
console.log((data as { name: string }).name);
}
When to use any versus unknown
| Situation | Recommended type | Reason |
|---|---|---|
| An external API response | unknown | The type is unclear, so prioritize safety |
| A JSON parse result | unknown | The structure is unknown, so a type check is needed |
| Migrating old code | any → unknown | Gradually increase type safety |
| When you truly cannot determine the type | unknown | Safer than any |
The principle for 2026: make unknown your first choice over any
Schema validation libraries (Zod / Valibot / class-validator and so on) have become widespread, and the mainstream approach is to always validate external data against a schema to determine its type. Because any disables type safety, treat it as forbidden in new code.
- ✅ Receive external data (API responses / JSON / user input) as
unknown, and make it a concrete type after schema validation - ✅ While migrating old code, replace with
unknownand narrow the type step by step - ❌ Avoid the "just to get it working"
any, which only piles up technical debt - ❌ Use
unknown[]/Record<string, unknown>instead ofany[]orRecord<string, any>
You can detect this statically with the @typescript-eslint/no-explicit-any rule in ESLint, or the lint/suspicious/noExplicitAny rule in Biome.
The void type (a type that indicates no value)
The void type represents the absence of a value. It is mainly used for function return values.
// void type: a function with no return value
function logMessage(message: string): void {
console.log(message);
// no return statement, or just return;
}
// Usage example
logMessage('Hello, TypeScript!'); // it just prints to the console
The difference between void and undefined
// A void-type function
function voidFunction(): void {
console.log('This returns nothing');
// return; this is also OK (returns nothing)
}
// An undefined-type function
function undefinedFunction(): undefined {
console.log('This returns undefined');
return undefined; // explicitly returns undefined (from TypeScript 5.1 onward, omitting the return statement is also allowed)
}
// Type compatibility
let v: void = undefined; // OK (undefined can be assigned to void)
// let u: undefined = voidFunction(); // Error (void cannot be assigned to undefined)
A practical example
// An event handler
function handleClick(event: MouseEvent): void {
console.log('Button clicked');
event.preventDefault();
}
// A type definition for a callback function
type Callback = (item: number) => void;
function forEach(arr: number[], callback: Callback): void {
for (let item of arr) {
callback(item);
}
}
forEach([1, 2, 3], (num) => {
console.log(num); // no return value needed
});
// Asynchronous processing
async function fetchData(): Promise<void> {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
// returns no value (side effects only)
}
The never type (a type that never returns a value)
The never type represents a function that never returns a value, or a point that is never reached.
// Example 1: a function that throws an exception (does not terminate normally)
function throwError(message: string): never {
throw new Error(message);
// the code after this does not run
}
// Example 2: an infinite loop
function infiniteLoop(): never {
while (true) {
console.log('This will never end');
}
// this point is never reached
}
The difference between never and void
// void: returns no value, but the function terminates normally
function voidFunc(): void {
console.log('Done');
// the function terminates
}
// never: the function never terminates
function neverFunc(): never {
throw new Error('Never returns');
// the function does not terminate (interrupted by the exception)
}
A use of the never type: exhaustiveness checks
An important use of the never type is an exhaustiveness check in a switch statement.
type Shape = 'circle' | 'square' | 'triangle';
function getArea(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI * 10 * 10;
case 'square':
return 10 * 10;
case 'triangle':
return (10 * 10) / 2;
default:
// if every case is handled, this becomes the never type
const exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}
If you add a new shape, a compile error occurs.
type Shape2 = 'circle' | 'square' | 'triangle' | 'hexagon';
function getArea2(shape: Shape2): number {
switch (shape) {
case 'circle':
return Math.PI * 10 * 10;
case 'square':
return 10 * 10;
case 'triangle':
return (10 * 10) / 2;
default:
// Error: Type '"hexagon"' is not assignable to type 'never'
// you can tell the hexagon case is not handled
const exhaustiveCheck: never = shape;
return exhaustiveCheck;
}
}
null and undefined
JavaScript has two types that represent "no value."
The difference between null and undefined
| Characteristic | null | undefined |
|---|---|---|
| Meaning | indicates a value is intentionally absent | a value has not been set |
| Assignment | assigned explicitly | the default value of an uninitialized variable |
| Use case | making "empty" explicit | a variable has not been initialized |
// undefined example
let value1: string | undefined;
console.log(value1); // undefined (not initialized)
// null example
let value2: string | null = null; // explicitly set to "empty"
The strictNullChecks option
When strictNullChecks: true in tsconfig.json, the handling of null and undefined becomes stricter.
// With strictNullChecks: true
// null and undefined cannot be assigned to each type
let name: string = 'John';
// name = null; // Error
// name = undefined; // Error
// To allow null or undefined, use a union type
let name2: string | null = 'John';
name2 = null; // OK
let name3: string | undefined = 'John';
name3 = undefined; // OK
let name4: string | null | undefined = 'John';
name4 = null; // OK
name4 = undefined; // OK
Safe access methods
// Optional chaining (the ?. operator)
let user: { name: string; email?: string } = {
name: 'John'
};
console.log(user.email?.toUpperCase()); // undefined (no error)
// Nullish coalescing (the ?? operator)
let email = user.email ?? 'no-email@example.com';
console.log(email); // 'no-email@example.com'
A practical example
// When a value may be absent, such as an API response
function getUserEmail(): string | null {
// actually fetch the data from an API
return null; // when no email is set
}
let userEmail: string | null = getUserEmail();
if (userEmail !== null) {
console.log(`Email: ${userEmail}`);
} else {
console.log('Email not found');
}
// Access safely with optional chaining
console.log(userEmail?.toUpperCase() ?? 'N/A');
Try it: implement a safe type guard ★★
Implement a function that meets the following requirements.
Requirements:
- Create a
processInputfunction - It takes an argument of the
unknowntype - If the argument is a string, convert it to uppercase and return it
- If the argument is a number, double it and return it
- If the argument is a boolean, invert it and return it
- Otherwise, return null
Hint
- Define the argument as the
unknowntype - Make the return type
string | number | boolean | null - Check the type with the
typeofoperator - Implement processing for each type
Answer and explanation
// A function that takes the unknown type and processes it based on the type
function processInput(value: unknown): string | number | boolean | null {
// string: convert to uppercase
if (typeof value === 'string') {
return value.toUpperCase();
}
// number: double it
if (typeof value === 'number') {
return value * 2;
}
// boolean: invert it
if (typeof value === 'boolean') {
return !value;
}
// otherwise: return null
return null;
}
// Tests
console.log(processInput('hello')); // 'HELLO'
console.log(processInput(21)); // 42
console.log(processInput(true)); // false
console.log(processInput([1, 2, 3])); // null
console.log(processInput({ a: 1 })); // null
console.log(processInput(undefined)); // null
Explanation:
- Using the
unknowntype lets you receive any value, but you need a type check before using it - Use the
typeofoperator to determine the type and process each type accordingly - Inside the type guard (
if (typeof value === 'string')), TypeScript automatically recognizes value as a string (type narrowing) - This approach is safer than using the
anytype, because you cannot operate on it without a type check
Comparison with using the any type:
// Using the any type (dangerous)
function processInputAny(value: any): string | number | boolean | null {
// you can operate on it without a type check
return value.toUpperCase(); // not an error, but a possible runtime error
}
// Using the unknown type (safe)
function processInputUnknown(value: unknown): string | number | boolean | null {
// return value.toUpperCase(); // Error: a type check is required
if (typeof value === 'string') {
return value.toUpperCase(); // OK
}
return null;
}
Summary
What you learned in this chapter:
- The any type: disables all type checking. Keep its use to a minimum
- The unknown type: a type-safe any. A type check is required before use
- The void type: used for functions with no return value
- The never type: used for functions that never return a value. Also used in exhaustiveness checks
- null/undefined: represent the absence of a value. Managed strictly with strictNullChecks
Key points:
- Use the unknown type rather than the any type
- Use type guards to narrow types safely
- Use the never type to perform exhaustiveness checks
In the next chapter, you will learn about union types, literal types, and enums.
Common pitfalls for beginners
Common mistakes
❌ Not understanding the difference between any and unknown
// ❌ Wrong: using any to ignore type checking
function processAny(value: any): string {
return value.toUpperCase(); // possible runtime error
}
processAny(123); // compiles, but a runtime error
// ✅ Correct: use unknown to force a type check
function processUnknown(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase(); // safe because it is after the type check
}
return 'Not a string';
}
Cause: any skips type checking entirely, but unknown requires a type check before use.
Solution: When the type is unknown, use unknown rather than any, and narrow it with a type guard.
❌ Confusing void and undefined
// ❌ Wrong: trying to return undefined from a void function
function logMessage(): void {
return undefined; // works, but the intent is unclear
}
// ✅ Correct: void expresses the intent of "the return value is not used"
function logMessage(): void {
console.log('Hello');
// no return statement, or just return;
}
Cause: void indicates "no return value," while undefined indicates "returns the value undefined."
Solution: Use void for functions whose return value is not used, and undefined when you need to explicitly return undefined.
❌ Forgetting a null check
// ❌ Wrong: not considering the possibility of null
function getLength(value: string | null): number {
return value.length; // Error: value is possibly 'null'
}
// ✅ Correct: do a null check
function getLength(value: string | null): number {
if (value === null) {
return 0;
}
return value.length;
}
// Or use optional chaining and nullish coalescing
function getLength(value: string | null): number {
return value?.length ?? 0;
}
Cause: When strictNullChecks is enabled, you cannot use a value that might be null directly.
Solution: Use a null check, optional chaining (?.), or nullish coalescing (??).
❌ Not understanding where to use never
// ❌ Wrong: adding a new case but forgetting to handle it
type Status = 'pending' | 'success' | 'error' | 'cancelled'; // added cancelled
function handleStatus(status: Status): string {
switch (status) {
case 'pending': return 'Processing';
case 'success': return 'Success';
case 'error': return 'Error';
// forgetting to handle cancelled!
}
}
// ✅ Correct: check exhaustiveness with never
function handleStatus(status: Status): string {
switch (status) {
case 'pending': return 'Processing';
case 'success': return 'Success';
case 'error': return 'Error';
case 'cancelled': return 'Cancelled';
default:
const exhaustiveCheck: never = status;
throw new Error(`Unhandled status: ${exhaustiveCheck}`);
}
}
Cause: Without an exhaustiveness check using the never type, you miss cases when you add a new one.
Solution: Assign to the never type in the switch default to detect omissions at compile time.
❌ Missing implicit any
// ❌ Wrong: declaring a variable without a type annotation
let data; // implicit any
data = 'Hello';
data = 123; // not a type error
// ✅ Correct: write an explicit type annotation
let data: string;
data = 'Hello';
// data = 123; // Error: Type 'number' is not assignable to type 'string'
Cause: A variable with no initial value and no type annotation becomes implicitly the any type.
Solution: Set noImplicitAny: true in tsconfig.json to forbid implicit any.