TypeScript

any, unknown, and type assertions

Three different ways to tell TypeScript "trust me" — and they carry very different amounts of actual risk. Knowing which one you're reaching for, and why, is what separates a deliberate escape hatch from a silent hole in your type safety.

Intermediate

4 min read

any: the type that disables checking entirely

let value: any = fetchExternalData();
 
value.whatever.deeply().nested; // compiles fine — any accepts literally any operation
value = 42;                     // fine
value = "now a string";          // also fine

any isn't "a very flexible type" — it's an explicit off-switch for type checking on that specific value. Every operation on an any-typed value compiles without complaint, regardless of whether it makes any real sense, and any is contagious: assigning an any value to another variable makes that variable any too, unless it's given an explicit type of its own. A codebase with any scattered through it has silent gaps in type safety wherever it appears — not bugs exactly, but places where TypeScript has simply stopped checking anything at all.

unknown: the genuinely safe version of "could be anything"

let value: unknown = fetchExternalData();
 
value.whatever; // Error: Object is of type 'unknown'
 
if (typeof value === "object" && value !== null && "id" in value) {
  console.log(value.id); // fine — narrowed enough to access .id safely
}

unknown also means "this could be any type" — but unlike any, the compiler refuses to let you do anything with an unknown value until you've actually narrowed it (the narrowing lesson's typeof/in/instanceof checks all work here) to something more specific. This is the correct type for genuinely unknown data — a JSON response from an external API, JSON.parse()'s return value, user input — anywhere the real shape isn't known upfront and shouldn't be blindly assumed.

Type assertions: telling TypeScript "I know more than you do, here"

const input = document.getElementById("email") as HTMLInputElement;
input.value; // fine — HTMLInputElement has .value; the general Element type doesn't
 
const wrong = ("hello" as unknown) as number; // compiles! — TypeScript trusts the assertion
wrong + 1; // runs, but produces NaN at runtime — the assertion lied, and nothing caught it

as (a type assertion, sometimes called a type cast, though it's meaningfully different from a real runtime cast) tells TypeScript "trust me, treat this value as this specific type" — it performs zero runtime conversion or validation, purely a compile-time instruction to stop checking one specific value the normal way. document.getElementById returns the general HTMLElement | null type since TypeScript can't know which specific element a given ID actually refers to — asserting as HTMLInputElement is a legitimate, common use, if you genuinely know that element is an input. The second example shows the real danger: an assertion can lie, and TypeScript has no way to catch that lie — going through unknown first (as unknown as number) even lets you assert between two genuinely incompatible types, bypassing TypeScript's normal "these types have nothing in common" safety check entirely.

The non-null assertion operator: a narrower, specific kind of assertion

function getUser(id: number): { name: string } | undefined {
  // ...
}
 
const user = getUser(1)!; // "trust me, this isn't undefined" — asserts away the | undefined
user.name; // fine, according to TypeScript — but a real runtime crash if getUser(1) actually returned undefined

! after a value (the non-null assertion operator) specifically strips null/undefined from a type, asserting "I know this isn't null or undefined here, even though the type says it could be." Like as, it performs no runtime check at all — if the assertion is wrong, the code crashes with a genuine runtime error (Cannot read properties of undefined) exactly where an explicit if (user) check would have caught it safely instead. ! is best reserved for cases with real, external certainty a type can't express (a DOM query on an element you're certain exists in the actual page) — not as a shortcut past an inconvenient type error.

The honest hierarchy, from safest to riskiest

unknown + narrowing   - genuinely type-safe, the compiler verifies every step
any                    - fully unchecked, contagious, spreads silently through a codebase
as SpecificType        - unchecked, but scoped to exactly one value, one place
value!                 - unchecked, scoped to exactly one value, specifically about null/undefined

None of these are "wrong" to use — real code genuinely needs escape hatches sometimes (DOM APIs, external data, gradually migrating a JavaScript codebase to TypeScript). The actual skill is choosing the narrowest, most honest one for what's actually known: unknown plus real narrowing whenever the shape can genuinely be verified, a scoped as/! assertion when there's real external certainty a type just can't express, and any treated as a last resort precisely because of how silently and widely it spreads once introduced.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why is `any` described as 'contagious'?

2. What's the key safety difference between `any` and `unknown` for a value of genuinely unknown shape (like a JSON API response)?

3. Does the `as` type assertion (e.g. `value as HTMLInputElement`) perform any runtime conversion or validation?