Skip to main content

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
info

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;
warning

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

SituationRecommended typeReason
An external API responseunknownThe type is unclear, so prioritize safety
A JSON parse resultunknownThe structure is unknown, so a type check is needed
Migrating old codeany → unknownGradually increase type safety
When you truly cannot determine the typeunknownSafer than any
warning

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 unknown and 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 of any[] or Record<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

Characteristicnullundefined
Meaningindicates a value is intentionally absenta value has not been set
Assignmentassigned explicitlythe default value of an uninitialized variable
Use casemaking "empty" explicita 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:

  1. Create a processInput function
  2. It takes an argument of the unknown type
  3. If the argument is a string, convert it to uppercase and return it
  4. If the argument is a number, double it and return it
  5. If the argument is a boolean, invert it and return it
  6. Otherwise, return null
Hint
  1. Define the argument as the unknown type
  2. Make the return type string | number | boolean | null
  3. Check the type with the typeof operator
  4. 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 unknown type lets you receive any value, but you need a type check before using it
  • Use the typeof operator 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 any type, 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

warning

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.