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.
- 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)
- Migrating a JS file: incremental
anywith// TODOandnoImplicitAnytightening. - Generic libraries escaping a compiler limit — prefer
unknown+ generics first. - Test mocks where exhaustive typing is noise — still prefer partial types.
as anyon 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.
Related on this site
- 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
- TypeScript: Everyday Types — any
- TypeScript: Narrowing
- TypeScript: unknown (handbook release notes / functions section)
- TypeScript ESLint: no-explicit-any
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.