The any, unknown, and never Types


The any, unknown, and never Types#
TypeScript's type system includes three special types that handle edge cases: any (the escape hatch that disables type checking), unknown (the type-safe alternative to any), and never (the type that represents values that never occur). Understanding these types is essential for writing robust TypeScript code that balances flexibility with safety.
Quick Summary: Use unknown instead of any when possible, and leverage
never for exhaustive checking. These types are the foundation for advanced
TypeScript patterns.
Understanding Special Types
TypeScript's type hierarchy includes these special types:
// Type hierarchy (simplified)
never → Bottom type (nothing is assignable to it except never)
↓
specific → string, number, boolean, etc.
↓
unknown → Top type (all types assignable to it)
↓
any → Escape hatch (opts out of type checking)Each serves a distinct purpose in TypeScript's type system.
The any Type
What is any?
any is TypeScript's escape hatch—it opts out of type checking completely:
let value: any;
value = 42; // ✓ OK
value = "hello"; // ✓ OK
value = true; // ✓ OK
value = { x: 10 }; // ✓ OK
value = [1, 2, 3]; // ✓ OK
// No type checking on operations
value.toUpperCase(); // ✓ No error (runtime error if value isn't a string)
value.nonExistentMethod(); // ✓ No error (runtime error)
let x: number = value; // ✓ No errorWhen you use any, TypeScript essentially treats it like JavaScript—no type checking at all.
Key characteristics:
- Can be assigned any value
- Can be assigned to any type
- All operations are allowed
- No autocomplete or type hints
When to Use any
Despite its dangers, any has legitimate use cases:
1. Migrating JavaScript to TypeScript:
// Temporarily use any during migration
let legacyData: any = getLegacyData();
// Plan to replace with proper types later
interface LegacyData {
// ... proper types
}2. Interacting with untyped third-party libraries:
// Third-party library without type definitions
declare const thirdPartyLib: any;
let result = thirdPartyLib.doSomething();3. Truly dynamic data structures:
// JSON parsing (though unknown is better)
let data: any = JSON.parse(jsonString);4. Prototyping or debugging:
// Quick prototype - fix types later
function quickTest(data: any) {
console.log(data);
}Important: These should be temporary solutions. Always aim to replace
any with proper types or at least unknown.
The Dangers of any
Using any defeats the purpose of TypeScript:
function processUser(user: any) {
// TypeScript allows this even though it might crash
console.log(user.name.toUpperCase());
return user.age * 2;
}
// These all compile without errors
processUser({ name: "Alice", age: 30 }); // Works
processUser({ name: null, age: 30 }); // Runtime error!
processUser({}); // Runtime error!
processUser(null); // Runtime error!Problems with any:
- Loses all type safety
- No IDE autocomplete
- Runtime errors aren't caught
- Makes refactoring dangerous
- Spreads through your codebase (type pollution)
Type pollution example:
function getData(): any {
return { name: "Alice" };
}
let data = getData(); // Type: any
let name = data.name; // Type: any (pollution spreads!)
let upper = name.toUpperCase(); // Type: any (still spreading!)Implicit any
TypeScript can infer any in some cases:
// Implicit any (with noImplicitAny: false)
function log(message) {
// Parameter 'message' implicitly has 'any' type
console.log(message);
}
let x; // Variable 'x' implicitly has type 'any'
x = 5;
x = "hello";Best Practice: Always enable noImplicitAny in your tsconfig.json to catch these cases:
{
"compilerOptions": {
"noImplicitAny": true
}
}The unknown Type
What is unknown?
unknown is the type-safe counterpart to any—you can assign anything to it, but you must check the type before using it:
let value: unknown;
value = 42; // ✓ OK
value = "hello"; // ✓ OK
value = true; // ✓ OK
value = { x: 10 }; // ✓ OK
// But you can't use it without checking
value.toUpperCase(); // ✗ Error: Object is of type 'unknown'
let x: number = value; // ✗ Error: Type 'unknown' is not assignable to type 'number'
// Must narrow the type first
if (typeof value === "string") {
value.toUpperCase(); // ✓ OK - TypeScript knows it's a string here
}Key characteristics:
- Can be assigned any value (like
any) - Cannot be assigned to other types without checking
- Cannot perform operations without type narrowing
- Forces you to handle types safely
Type Narrowing with unknown
You must narrow unknown to a specific type before using it:
1. Using typeof:
function processValue(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // value is string here
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // value is number here
} else if (typeof value === "boolean") {
console.log(value ? "yes" : "no"); // value is boolean here
}
}2. Using instanceof:
function handleError(error: unknown) {
if (error instanceof Error) {
console.log(error.message); // error is Error here
} else {
console.log("Unknown error:", error);
}
}3. Using type guards:
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // value is string
} else if (isObject(value)) {
console.log(Object.keys(value)); // value is object
}
}4. Using in operator:
function processData(data: unknown) {
if (
typeof data === "object" &&
data !== null &&
"name" in data &&
"age" in data
) {
// data is object with name and age properties
let obj = data as { name: unknown; age: unknown };
console.log(obj.name, obj.age);
}
}Common Patterns with unknown
JSON parsing:
function parseJSON(jsonString: string): unknown {
return JSON.parse(jsonString);
}
let data = parseJSON('{"name": "Alice", "age": 30}');
// Must validate before use
if (
typeof data === "object" &&
data !== null &&
"name" in data &&
"age" in data
) {
let user = data as { name: string; age: number };
console.log(user.name); // Safe to use
}API responses:
async function fetchData(url: string): Promise<unknown> {
let response = await fetch(url);
return response.json();
}
let data = await fetchData("/api/users");
// Validate structure
function isUser(value: unknown): value is { name: string; age: number } {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof (value as any).name === "string" &&
"age" in value &&
typeof (value as any).age === "number"
);
}
if (isUser(data)) {
console.log(data.name); // Type-safe
}Error handling:
try {
riskyOperation();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else if (typeof error === "string") {
console.error(error);
} else {
console.error("An unknown error occurred");
}
}unknown vs any
Comparing the two:
// any - unsafe
let anyValue: any = "hello";
anyValue.toFixed(2); // No error, but runtime crash!
// unknown - safe
let unknownValue: unknown = "hello";
unknownValue.toFixed(2); // ✗ Error: Object is of type 'unknown'
if (typeof unknownValue === "number") {
unknownValue.toFixed(2); // ✓ OK - type checked first
}When to use each:
| Use Case | Use any | Use unknown |
|---|---|---|
| Migrating JS code | ✓ Temporary | ✓ Better |
| JSON parsing | ✗ | ✓ |
| Error handling | ✗ | ✓ |
| Truly dynamic data | ✗ | ✓ |
| Quick prototyping | ✓ | ✗ |
| Third-party untyped libs | ✓ | ✓ |
Rule of Thumb: Always prefer unknown over any. Only use any when you
have no other choice and plan to fix it later.
The never Type
What is never?
never represents values that never occur—it's the bottom type in TypeScript's type system:
// Function that never returns normally
function throwError(message: string): never {
throw new Error(message);
}
// Infinite loop - never returns
function infiniteLoop(): never {
while (true) {
// Loop forever
}
}
// Variable that can never have a value
let impossible: never;
impossible = 5; // ✗ Error: Type 'number' is not assignable to type 'never'
impossible = "hi"; // ✗ Error: Type 'string' is not assignable to type 'never'Key characteristics:
- Represents values that never occur
- No value can be assigned to
never(exceptneveritself) nevercan be assigned to any type- Used for unreachable code and exhaustive checking
Functions That Never Return
Functions return never when they can't complete normally:
// Throws an error
function fail(message: string): never {
throw new Error(message);
}
// Infinite loop
function keepProcessing(): never {
while (true) {
processNextTask();
}
}
// Process exits
function exit(code: number): never {
process.exit(code);
}
// Using never in control flow
function processValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
}
// TypeScript knows this is unreachable
fail("Invalid value type");
// No return needed - never indicates function doesn't return
}Exhaustive Type Checking
never enables exhaustive checking in switch statements and conditionals:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
// If we forget a case, this catches it
let exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}Adding a new type:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number }; // New!
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
// ✗ Error: Type 'triangle' is not assignable to type 'never'
let exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}
// TypeScript forces us to handle the triangle case!Best Practice: Always use exhaustive checking in switch statements for union types. It catches bugs when you add new cases.
Helper function for exhaustive checks:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
return assertNever(shape); // Compile error if not exhaustive
}
}Impossible Intersections
never appears when type intersections are impossible:
type StringAndNumber = string & number; // Type: never
// Can't be both string AND number
type A = { type: "a"; value: string };
type B = { type: "b"; value: number };
type Impossible = A & B; // Type: never
// Can't have type "a" and type "b" simultaneously
// Useful in conditional types
type OnlyStrings<T> = T extends string ? T : never;
type Result = OnlyStrings<string | number | boolean>;
// Type: string (number and boolean become never)Filtering with never:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | number | null | undefined>;
// Type: string | numberPractical Examples
API Response Handling
interface ApiResponse<T> {
status: "success" | "error";
data?: T;
error?: string;
}
async function fetchUser(id: number): Promise<ApiResponse<unknown>> {
try {
let response = await fetch(`/api/users/${id}`);
let data = await response.json();
return { status: "success", data };
} catch (error: unknown) {
if (error instanceof Error) {
return { status: "error", error: error.message };
}
return { status: "error", error: "Unknown error" };
}
}
// Using the response
let response = await fetchUser(1);
if (response.status === "success" && response.data) {
let data = response.data;
// Validate structure
if (
typeof data === "object" &&
data !== null &&
"name" in data &&
"email" in data
) {
let user = data as { name: string; email: string };
console.log(`User: ${user.name} (${user.email})`);
}
} else {
console.error("Error:", response.error);
}Error Handling
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(
message: string,
public field: string
) {
super(message);
this.name = "ValidationError";
}
}
function handleError(error: unknown): never {
if (error instanceof NetworkError) {
console.error(`Network error (${error.statusCode}):`, error.message);
} else if (error instanceof ValidationError) {
console.error(`Validation error in ${error.field}:`, error.message);
} else if (error instanceof Error) {
console.error("Error:", error.message);
} else {
console.error("Unknown error:", error);
}
process.exit(1); // Function never returns
}
try {
riskyOperation();
} catch (error: unknown) {
handleError(error); // Returns never, so no code after this runs
}Type Guards
interface User {
type: "user";
name: string;
email: string;
}
interface Admin {
type: "admin";
name: string;
email: string;
permissions: string[];
}
type Account = User | Admin;
function isAdmin(account: unknown): account is Admin {
return (
typeof account === "object" &&
account !== null &&
"type" in account &&
account.type === "admin" &&
"permissions" in account &&
Array.isArray((account as Admin).permissions)
);
}
function processAccount(account: unknown) {
if (isAdmin(account)) {
// TypeScript knows account is Admin here
console.log(`Admin: ${account.name}`);
console.log(`Permissions: ${account.permissions.join(", ")}`);
} else {
console.log("Not an admin account");
}
}State Machines
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: string };
function renderState(state: State): string {
switch (state.status) {
case "idle":
return "Ready to start";
case "loading":
return "Loading...";
case "success":
return `Data: ${state.data}`;
case "error":
return `Error: ${state.error}`;
default:
// Exhaustive check
let exhaustive: never = state;
throw new Error(`Unhandled state: ${exhaustive}`);
}
}
// Using the state machine
let currentState: State = { status: "idle" };
console.log(renderState(currentState)); // "Ready to start"
currentState = { status: "loading" };
console.log(renderState(currentState)); // "Loading..."
currentState = { status: "success", data: "Hello, World!" };
console.log(renderState(currentState)); // "Data: Hello, World!"Type Hierarchy
Understanding the relationships:
// never is assignable to everything
let n: never = getNever();
let str: string = n; // ✓ OK
let num: number = n; // ✓ OK
let obj: object = n; // ✓ OK
// Nothing (except never) is assignable to never
let n2: never;
n2 = 5; // ✗ Error
n2 = "hello"; // ✗ Error
n2 = getNever(); // ✓ OK
// Everything is assignable to unknown
let u: unknown;
u = 5; // ✓ OK
u = "hello"; // ✓ OK
u = { x: 10 }; // ✓ OK
// unknown is not assignable to other types (without checking)
let u2: unknown = "hello";
let str2: string = u2; // ✗ Error
// any breaks the type system
let a: any = 5;
let str3: string = a; // ✓ OK (but unsafe!)
a = "hello";
let num2: number = a; // ✓ OK (but unsafe!)Subtype relationships:
never
↓ (subtype of)
literal types (e.g., "hello", 42)
↓
primitive types (string, number, etc.)
↓
object types
↓
unknown
any (breaks the hierarchy)
Migration Strategies
Migrating from any to unknown:
Find all any usages:
# Search your codebase
grep -r ": any" src/Replace with unknown:
// Before
function process(data: any) {
return data.value;
}
// After
function process(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: unknown }).value;
}
throw new Error("Invalid data");
}
Add type guards:
function isValidData(data: unknown): data is { value: string } {
return (
typeof data === "object" &&
data !== null &&
"value" in data &&
typeof (data as any).value === "string"
);
}
function process(data: unknown) {
if (isValidData(data)) {
return data.value; // Type-safe!
}
throw new Error("Invalid data");
}Test thoroughly:
// Add tests for edge cases
process({ value: "hello" }); // Works
process({}); // Throws
process(null); // Throws
process("string"); // ThrowsBest Practices
1. Prefer unknown over any:
// ✗ Avoid
function parse(json: string): any {
return JSON.parse(json);
}
// ✓ Better
function parse(json: string): unknown {
return JSON.parse(json);
}2. Use type guards with unknown:
// ✓ Good
function process(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase();
}
throw new Error("Expected string");
}3. Use never for exhaustive checking:
// ✓ Good - catches missing cases at compile time
function handle(value: "a" | "b") {
switch (value) {
case "a":
return "A";
case "b":
return "B";
default:
let exhaustive: never = value;
throw new Error(`Unhandled: ${exhaustive}`);
}
}4. Enable strict compiler options:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}5. Document why you use any:
// If you must use any, explain why
let data: any; // TODO: Add proper types after API stabilizesCommon Mistakes
Mistake 1: Using any when unknown would work:
// ✗ Bad
function log(data: any) {
console.log(data);
}
// ✓ Better
function log(data: unknown) {
console.log(data); // console.log accepts unknown
}Mistake 2: Not narrowing unknown before use:
// ✗ Bad
function process(data: unknown) {
return data.value; // Error!
}
// ✓ Good
function process(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: unknown }).value;
}
}Mistake 3: Forgetting exhaustive checks:
type Status = "pending" | "active" | "done";
// ✗ Risky - no compile error if you add a new status
function getMessage(status: Status): string {
if (status === "pending") return "Pending...";
if (status === "active") return "Active!";
if (status === "done") return "Done!";
return "Unknown"; // Silently handles new cases
}
// ✓ Better - compile error if you add a new status
function getMessage(status: Status): string {
switch (status) {
case "pending":
return "Pending...";
case "active":
return "Active!";
case "done":
return "Done!";
default:
let exhaustive: never = status;
throw new Error(`Unhandled: ${exhaustive}`);
}
}Mistake 4: Assigning never inappropriately:
// ✗ Bad - never should be for truly impossible values
let value: never = 42; // Error: Type 'number' is not assignable to type 'never'
// ✓ Good - never for functions that don't return
function throwError(): never {
throw new Error();
}Next Steps
You now understand TypeScript's special types and how to use them effectively! Here's what to explore next:
Practice These Concepts:
- Replace
anywithunknownin your existing code - Add exhaustive checking to your switch statements
- Create type guards for common data structures
- Use
neverto catch missing cases at compile time
Continue Learning:
- Type guards and type predicates
- Advanced union and intersection types
- Conditional types
- Mapped types and utility types
Congratulations! You've mastered any, unknown, and never. These
types are fundamental to writing safe, flexible TypeScript code. Use unknown
by default, leverage never for exhaustive checking, and reserve any only
for true edge cases.
Questions about these special types? Struggling with type narrowing? Share your use case in the comments and let's work through it together!