unknown over any, and never lying to the compiler
You now know enough TypeScript to make the compiler happy. This module is about the gap between code
that compiles and code a team is glad you wrote — the difference that decides whether you are worth
hiring beyond a first task. It starts with the single most important habit: never lie to the compiler,
and prefer unknown over any every time.
any is a lie
Every any you write is a promise: "trust me, I know what this is, stop checking." When the promise is
false — and it often is — you have reintroduced exactly the runtime crashes TypeScript exists to prevent:
function process(data: any) {
return data.user.name.toUpperCase(); // compiles fine
}
process({ user: null }); // runtime crash: Cannot read properties of null
process("hello"); // runtime crash: hello.user is undefined
any made every line compile and every line a runtime bug. This is the core problem: any does not
make a type problem go away; it hides it until runtime. A codebase sprinkled with any gets none of
TypeScript's benefit — it is JavaScript with extra ceremony, and it fails in production exactly where the
types were switched off.
The instinct to reach for any almost always comes from one place: a red error you want gone. any makes
the error disappear — but the bug it was warning about is still there, now invisible. Making an error
message go away is not the same as fixing the problem, and any is the tool for the former disguised as
the latter.
unknown — honest about not knowing
When you genuinely do not know a value's type — external data, a dynamic value — the honest type is
unknown, not any. Both accept any value; the difference is what happens next:
function process(data: unknown) {
data.user.name; // error: 'data' is of type 'unknown'
if (typeof data === "object" && data !== null && "user" in data) {
// now you have PROVEN something about data before using it
}
}
unknown says "I do not know what this is, so make me check." It permits nothing until you narrow — so it
keeps the safety any throws away. The bug that any hid (accessing .user on something that might not
have it) becomes a compile error with unknown, forcing you to handle it. unknown is the right type
almost everywhere you were tempted by any, because it captures "I do not know the type yet" without
abandoning the guarantee.
The mechanical rule: when you want to write any, write unknown and then narrow. It is a few more
characters and it keeps the compiler on your side.
When any is genuinely acceptable — rarely, and deliberately
"Never any" as an absolute is itself a mistake (a myth the first module cleared up). There are a few
honest uses:
- A migration in progress. Adding TypeScript to a large JavaScript codebase, a temporary
anyat an unconverted boundary is pragmatic — with a// TODO: type thiscomment and a plan to remove it. - A genuinely dynamic value that resists typing, where
unknownplus narrowing would be disproportionate machinery for a tiny, contained case — and even then,unknownis usually still better. - A third-party type that is wrong, where you must override it locally. Prefer a precise override, but
a commented
anyis sometimes the pragmatic escape.
The common thread: deliberate, localised, and commented. An any that is a conscious decision,
confined to one spot, with a comment explaining why, is defensible. An any reached for reflexively to
silence an error is not. If you cannot write a one-line comment justifying an any, you should not be
writing it.
Configure the compiler to catch lies
Two settings turn "prefer not to lie" into "cannot lie by accident":
strict: true— enables the whole family of strict checks, includingnoImplicitAny(an un-annotated parameter, which would be a silentany, becomes an error) andstrictNullChecks. Turn strict mode on from day one — the strict-mode lesson in the real-projects module is emphatic about this.- An ESLint rule —
@typescript-eslint/no-explicit-anyflags every explicitany, so each one is a conscious choice you must acknowledge (or disable with a comment). Many teams enable it, precisely to makeanydeliberate rather than habitual.
With these on, an accidental any is caught, and a deliberate one is visible — which is exactly the
posture you want.
The unknown-based validation pattern
The place you should use unknown constantly is the boundary — external data entering your program:
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json(); // unknown — the API could send anything
if (!isUser(data)) {
throw new Error("Invalid user data from API");
}
return data; // narrowed to User by the guard — now safe
}
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null
&& "id" in value && "name" in value;
}
response.json() returns any by default (a historical wart) — the first thing to do is assign it to
unknown, which forces validation before you trust it. The type guard (isUser) narrows it, and only
then do you return a User. This is the honest way to handle external data: treat it as unknown,
validate it, then use the validated type — never annotate incoming data with the shape you hope it has
and skip the check (the "TypeScript does not validate data" myth). A schema library like Zod (real-projects
module) does this validation for you, and returns a typed value.
Check your work
Why any is a lie. It tells the compiler to stop checking, reintroducing the runtime crashes
TypeScript prevents — hiding the bug until runtime.
Where the instinct for any comes from, and why it is wrong. Silencing a red error — but that hides
the warned-about bug rather than fixing it.
Why unknown is the honest alternative. It accepts any value but permits nothing until you narrow —
keeping the safety any discards.
The mechanical rule. When tempted to write any, write unknown and narrow.
When any is acceptable. Rarely — a migration boundary, a genuinely dynamic contained value, a wrong
third-party type — and always deliberate, localised, and commented.
The test for an acceptable any. Can you write a one-line comment justifying it? If not, do not write
it.
The two settings that catch lies. strict: true (including noImplicitAny) and the ESLint
no-explicit-any rule.
The external-data pattern. Treat incoming data as unknown, validate it (a guard or Zod), then use
the validated type.
Practice
- Write a
process(data: any)that crashes at runtime on bad input. Confirm it compiles despite the bug. - Change
anytounknownand confirm the bug is now a compile error. Narrow before using the data. - Take a red error you would fix with
anyand instead fix it properly (narrow, or correct the type). Compare. - Enable
noImplicitAny(or strict) and write an un-annotated parameter. Read the error. - Enable the ESLint
no-explicit-anyrule and confirm every explicitanyis flagged. - Write the
fetchUserboundary pattern:unknownfromjson(), a guard, then a validated return. - For three
anys you can find (or imagine), decide whether each is a justifiable deliberate use or a lie to silence an error.
Official documentation
- TypeScript — The
anytype — What it disables. - TypeScript —
unknown— The safe alternative. - typescript-eslint — no-explicit-any — The lint rule.
Next: avoiding as and !, and what to do instead.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship