Type system6 min read

TypeScript any, unknown, and never Have Different Boundaries

Choose any, unknown, or never by what callers may supply, what code may do with the value, and whether a value can exist.

  • type system
  • type narrowing
  • API design

unknown accepts every value but permits almost no operations until you narrow it. never represents a value that cannot occur. any opts out of checking in both directions. They sit at different boundaries, so replacing one with another changes what an API promises.

That distinction is easiest to see through assignability. Under strictNullChecks, TypeScript’s assignability table for any, unknown, and never states the core rules:

  • Every type is assignable to unknown.
  • unknown is assignable only to unknown and any without narrowing or an assertion.
  • never is assignable to every type, but no type other than never is assignable to it.
  • any is assignable to and from almost every type. The important exception is that any is not assignable to never.

These are compiler rules. None of the three adds a runtime check.

Use unknown when the value is real but untrusted

An input typed as unknown can hold anything. The function must establish a useful fact before reading a property, calling it, or assigning it to a narrower type.

function describe(value: unknown): string {
  if (typeof value === 'string') {
    return value.trim();
  }

  if (typeof value === 'number' && Number.isFinite(value)) {
    return value.toFixed(2);
  }

  return 'unsupported value';
}

The checks do runtime work, and the compiler uses their results to narrow value. This makes unknown a sound default for parsed JSON, caught values, message payloads, and plugin output. The TypeScript 3.0 release notes introduced it as the type-safe counterpart to any.

unknown is also useful for values that code only stores or forwards:

type Envelope = {
  topic: string;
  payload: unknown;
};

function relay(message: Envelope, send: (message: Envelope) => void) {
  send(message);
}

relay does not need to know the payload shape. A later consumer can validate it. Using a generic would make sense if the caller and consumer needed to preserve one known payload type through the API. unknown says the relationship is deliberately unavailable here.

any disables checking and can spread

Code may read, call, construct, index, and assign a value of type any without proving that the operation is valid.

declare const response: any;

const count: number = response.result.count;
response.retry(3);

Both lines compile. Either can fail at runtime. The handbook’s any documentation describes this as disabling further checking for the value.

The escape also propagates through many expressions:

declare const dynamic: any;

const result = dynamic.account.profile.name;
// result is any

Use any when unchecked interoperability is the actual contract, such as a short-lived JavaScript migration seam or a declaration for an API that cannot be described yet. Keep that seam narrow. Converting the value to unknown at the boundary forces the rest of the program to recover evidence instead of inheriting the opt-out.

declare function legacyRead(): any;

const unchecked = legacyRead();
const input: unknown = unchecked;

This assignment cannot validate the runtime value. It only stops unchecked operations from spreading beyond input.

never marks an impossible result

Functions that always throw or cannot finish can return never:

function fail(message: string): never {
  throw new Error(message);
}

function requireToken(token: string | undefined): string {
  return token ?? fail('missing token');
}

Because fail cannot produce a value, the other branch determines the expression’s result. never also appears after control flow removes every member of a union. That makes it useful for exhaustive checks:

type Job =
  | { kind: 'email'; address: string }
  | { kind: 'report'; reportId: number };

function run(job: Job): void {
  switch (job.kind) {
    case 'email':
      console.log(job.address);
      return;
    case 'report':
      console.log(job.reportId);
      return;
    default:
      job satisfies never;
  }
}

Adding another Job variant makes the satisfies expression fail until the switch handles it. The value exists at runtime only if the static model and implementation disagree.

Do not use never for a function that returns no useful value but does finish. That function returns void. A Promise that always rejects can be written as Promise<never>, although its rejection reason still exists at runtime and TypeScript does not type rejection channels.

Top and bottom describe direction, not safety

unknown is a top type because every value type can flow into it. never is a bottom type because it can flow into every value type. Those labels describe the assignability order.

They also explain simple type identities:

type A = string | never;   // string
type B = string & unknown; // string

An impossible union member adds no possible values. Intersecting with a type that accepts every value removes nothing. any does not follow this clean model because it exists to suspend normal checking.

At an external input boundary, start with unknown and narrow or parse it. Use never to encode paths and cases that should not produce a value. Reserve any for a conscious escape where losing the check is part of the job.