ESC

Type to search the knowledge base.

unknown vs any

Why unknown is the type-safe top type, how to narrow it, and when any is a deliberate escape hatch — not a default.

beginner4 min read
  • typescript
  • unknown
  • any
  • type-safety

any and unknown both accept every value. The difference is what you may do with the value afterward. any turns off checking. unknown forces you to narrow before use. For frontend code that parses JSON, reads localStorage, or handles event.target, that distinction is the whole game.

Docs: TypeScript Handbook — unknown, any, Narrowing.

The one-sentence rule

Type Assignability in What you can do without checks
any Everything Essentially everything (no safety)
unknown Everything Almost nothing — must narrow first
function takeAny(x: any) {
  x.foo.bar.baz(); // compiles; may explode at runtime
}

function takeUnknown(x: unknown) {
  // x.foo; // error
  if (typeof x === 'string') {
    console.log(x.toUpperCase()); // ok — narrowed
  }
}

Prefer unknown for values that are “typed as data from outside the type system.” Prefer proper types once validated. Use any only as a temporary escape hatch with a plan to remove it.

Where unknown shines

JSON.parse

const raw: unknown = JSON.parse(text);

function isUser(v: unknown): v is { id: string; name: string } {
  return (
    typeof v === 'object' &&
    v !== null &&
    'id' in v &&
    'name' in v &&
    typeof (v as { id: unknown }).id === 'string' &&
    typeof (v as { name: unknown }).name === 'string'
  );
}

if (isUser(raw)) {
  console.log(raw.name);
}

Typing JSON.parse as a generic T without validation is a lie — the runtime value isn’t magically T.

DOM and events

function onClick(e: Event) {
  const t = e.target;
  if (t instanceof HTMLInputElement) {
    console.log(t.value);
  }
}

EventTarget is not an HTMLInputElement until you prove it. That’s the same discipline as unknown.

localStorage / search params

function readTheme(): 'light' | 'dark' | null {
  const v: unknown = localStorage.getItem('theme');
  // getItem returns string | null — still validate domain
  if (v === 'light' || v === 'dark') return v;
  return null;
}

Narrowing patterns

function handle(x: unknown) {
  if (typeof x === 'string') { /* string */ }
  if (typeof x === 'number') { /* number */ }
  if (x === null) { /* null */ }
  if (Array.isArray(x)) { /* unknown[] */ }
  if (x instanceof Date) { /* Date */ }
  if (typeof x === 'object' && x !== null) {
    // object, not null — still narrow fields
  }
}

User-defined type guards (v is Foo) and assertion functions (asserts v is Foo) scale better than cast spam.

function assertString(v: unknown): asserts v is string {
  if (typeof v !== 'string') throw new Error('expected string');
}

Libraries like Zod produce typed outputs from unknown inputs — validation as the boundary.

Why any spreads

any is infectious:

const data: any = fetchData();
const name = data.user.name; // name is any
const upper = name.toUpperCase(); // still unchecked

One any at the API boundary can erase safety through half the UI. unknown stops the chain at the first use until you narrow.

any also silences typos:

const u: any = { name: 'Ada' };
u.nmae; // no error

Legitimate any uses (rare)

  1. Migrating a JS file: incremental any with // TODO and noImplicitAny tightening.
  2. Generic libraries escaping a compiler limit — prefer unknown + generics first.
  3. Test mocks where exhaustive typing is noise — still prefer partial types.
  4. as any on a single expression to unblock, with a comment and issue link — never on whole modules.

If your default is any, you are writing JavaScript with extra steps.

any vs type assertions

const v = payload as User;     // trust me
const u = payload as any as User; // double lie

Assertions skip validation. Prefer:

const user = parseUser(payload); // throws or Result

unknown + parse is honest; as User on JSON is not.

ESLint / tsconfig allies

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "useUnknownInCatchVariables": true
  }
}
try {
  // …
} catch (e) {
  // e is unknown under useUnknownInCatchVariables
  if (e instanceof Error) console.error(e.message);
}

Lint rules: @typescript-eslint/no-explicit-any (warn or error), ban Object / prefer unknown for empty values.

Interview angle

Both are top types; only unknown requires narrowing. Show JSON.parse typed as unknown + type guard. Explain infection of any. Mention useUnknownInCatchVariables.

  • Generics basics
  • XSS — untrusted strings are data until proven otherwise; same boundary mindset
  • Core Web Vitals — unrelated metric-wise; typing API payloads still reduces prod surprises

Further reading

Related guides