TypeScript any vs unknown — The Answer Reddit Gives Wrong
Someone asks how to type JSON.parse results. Top comment: “just use any.” That silences the compiler and every caller. The type you want at trust boundaries is unknown: “this value exists, but you must narrow before use.” any means “turn off checking for this and anything it touches.”
Behavioral difference in one snippet
function takeAny(x: any) {
x.foo.bar(); // ok for TS — runtime may explode
}
function takeUnknown(x: unknown) {
x.foo.bar(); // error
if (
typeof x === "object" &&
x !== null &&
"foo" in x &&
typeof (x as { foo: unknown }).foo === "object"
) {
// still better: use a type guard / zod
}
}
unknown is a type-safe top type. any is an escape hatch that propagates.
Where unknown belongs
| Boundary | Pattern |
|---|---|
JSON.parse | unknown → validate |
req.body | unknown → schema |
localStorage.getItem then parse | unknown |
| External widget callbacks | unknown params |
error in catch | unknown (TS 4.4+) |
function parseConfig(raw: string): Config {
const data: unknown = JSON.parse(raw);
return ConfigSchema.parse(data); // zod/yup/valibot
}
If you refuse a schema library, write a type guard:
function isConfig(v: unknown): v is Config {
return (
typeof v === "object" &&
v !== null &&
"apiUrl" in v &&
typeof (v as { apiUrl: unknown }).apiUrl === "string"
);
}
Where any still appears (narrowly)
- Migrating a JS file and you need a temporary bridge — track with
// TODOand eslint@typescript-eslint/no-explicit-any. - Typing third-party modules with broken types — prefer
unknownor a minimal local interface overany. - Generic libraries that truly accept anything and forward it — even then
unknown+ generics often wins.
// prefer
function identity<T>(x: T): T { return x; }
// not
function identity(x: any): any { return x; }
Why Reddit’s “cast to any” fix is costly
const el = document.querySelector("#app") as any;
el.innerHTML = userHtml; // also a security smell
You lost null checking, DOM typing, and review signal. Prefer:
const el = document.querySelector("#app");
if (!(el instanceof HTMLElement)) throw new Error("#app missing");
el.replaceChildren(/* safe nodes */);
as unknown as T double assertion
This is the “I know better than the compiler” hammer:
const t = value as unknown as Target;
Use only when you have a proven invariant the type system cannot express — and document it. Prefer refactoring types so a single assertion or guard suffices. Double assertions are any with extra steps.
ESLint defaults that help
{
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error"
}
}
Teams that ban any but allow unchecked as will cheat. Gate both.
Mental rule
unknown: inbound data you do not trust yet.- Concrete types: after validation.
any: intentional debt with an owner and an expiry.
Gradual adoption
Turn on no-explicit-any as warn first, then error per package. Replace high-churn any at API boundaries before chasing cosmetic locals. Require a one-line reason on every eslint-disable for any. Pairing unknown with a zod (or similar) schema at the boundary deletes entire classes of as casts downstream — invest there before sprinkling generics everywhere.
The wrong Reddit answer optimizes for fewer red squiggles. The right one optimizes for failures at the boundary instead of in production business logic.