any, unknown and never

TypeScript · Essentials · 10 min

Choosing unknown over any is one of the biggest differences between safe and leaky TypeScript.

`any` opts a value out of type checking. You can call anything on it, assign it anywhere, and the compiler stays silent. Worse, any spreads: const n = anyValue.count is also any, and so on through your program. It exists for migrating JavaScript and for rare escape hatches; with noImplicitAny (part of strict) the compiler at least stops inventing it for unannotated parameters.

`unknown` is the safe "could be anything" type. Every value can be assigned to unknown, but you can't do anything with an unknown until you narrow it:

function handle(data: unknown) {
  data.toUpperCase();                 // error
  if (typeof data === "string") data.toUpperCase(); // ok
}

Use unknown for data whose shape you haven't verified: parsed JSON, postMessage payloads, values caught in catch (with strict, catch (e) gives e: unknown). Validate it — by hand or with a schema library — and the narrowed result is properly typed.

`never` is the empty type: no value has it. It shows up as:

  • the return type of functions that always throw,
  • what's left after narrowing has eliminated every possibility,
  • the result of impossible intersections like string & number.

That second use powers exhaustiveness checks: in the default branch of a switch over a union, assign the value to a never variable. If someone later adds a new member to the union, that assignment stops compiling and points you to the switch you forgot to update.

Rule of thumb: never write any when unknown would do.

Example

function parsePort(raw: string): number {
  const data: unknown = JSON.parse(raw);         // don't trust it yet
  if (typeof data === "object" && data !== null && "port" in data &&
      typeof data.port === "number") {
    return data.port;                             // narrowed to number
  }
  throw new Error("invalid config");
}

type Status = "active" | "paused" | "closed";
function label(s: Status): string {
  switch (s) {
    case "active": return "Running";
    case "paused": return "On hold";
    case "closed": return "Done";
    default: {
      const unreachable: never = s;    // breaks if Status gains a member
      return unreachable;
    }
  }
}

try { parsePort("{}"); } catch (e) {             // e: unknown
  console.log(e instanceof Error ? e.message : String(e), label("paused"));
}

Remember

  • any disables checking and spreads; avoid it
  • unknown accepts anything but must be narrowed before use
  • never = no possible value; use it for exhaustiveness checks

Watch out for

  • Typing parsed JSON as any and letting unchecked data flow through the program.

Check yourself

  1. data is typed unknown. Which line compiles?
  2. In the default branch of an exhaustive switch over a union, what type does the narrowed variable have?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/functions.html#unknown

any, unknown and never · TypeScript · Topolearn