RizTech Academy logo
RizTech Academy
Writing TypeScript Worth ReadingLesson 2 of 525 min

Avoiding as and !, and what to do instead

any turns off checking for a value's whole life. Two smaller tools turn it off for a single moment: the type assertion as, and the non-null assertion !. Both tell the compiler "I know better than you here" — and both are, like any, usually a sign you should have done something safer. This lesson is when they are genuinely justified (rarely) and what to do instead (almost always).

as — a type assertion overrides the compiler

value as Type tells TypeScript "treat this value as Type, regardless of what you inferred":

const input = document.getElementById("age") as HTMLInputElement;
input.value;    // fine — you asserted it is an input, so .value exists

getElementById returns HTMLElement | null (it might not find the element), but you assert it is an HTMLInputElement so you can use .value. This is you overriding the compiler's knowledge — and the danger is obvious: if you are wrong, there is no check. If that element is actually a <div> (or does not exist), input.value is undefined or a crash, and TypeScript said nothing, because you told it not to.

as does not convert or validate anything at runtime — it only changes what the compiler believes. It is a promise, exactly like any, just scoped to one expression. And like any, the instinct to reach for it is usually a red error you want gone.

Why as is usually the wrong fix

Consider the common misuse:

const data = JSON.parse(input) as User;    // WRONG — asserting, not checking
data.name.toUpperCase();                    // crashes if the data is not actually a User

JSON.parse returns any; asserting as User tells the compiler "this is a User" without checking the data. If the JSON does not match User, you get a runtime crash and no warning. This is the same "you did not validate external data" mistake, dressed as a type assertion. The right fix is to validate:

const parsed: unknown = JSON.parse(input);
if (isUser(parsed)) {
  parsed.name.toUpperCase();   // safe — narrowed by a real check
}

Narrowing with a type guard (or Zod) proves the type at runtime; as merely asserts it. Prefer narrowing over asserting: a guard checks and TypeScript trusts the check; an assertion skips the check and TypeScript trusts you. Almost every as on external data should be a validation instead.

The double-assertion smell

The clearest sign an assertion has gone wrong is when TypeScript refuses even the assertion, and you force it through unknown:

const x = someValue as unknown as User;   // a "double assertion" — a loud code smell

TypeScript only allows as between types that plausibly overlap. When it rejects an assertion ("Conversion of type X to type Y may be a mistake"), the fix is not to launder it through as unknown as — that silences a warning that TypeScript was right to give. A double assertion means "I am overriding the compiler's objection that these types are unrelated", which is almost always a design problem, not something to force. When you see as unknown as, treat it as a red flag to reconsider, not a technique.

! — the non-null assertion

value! asserts "this is not null or undefined here", removing null/undefined from the type:

function greet(name: string | null) {
  return name!.toUpperCase();    // asserts name is not null — crashes if it is
}

greet(null);   // runtime crash: Cannot read properties of null

name! strips the null so you can call .toUpperCase() — but if name is null, you get the exact crash TypeScript was preventing. ! is the non-null version of as: a promise that overrides the compiler, unchecked. And it has the same problem — it reintroduces the billion-dollar mistake on your own authority. A trail of ! through code (user!.address!.city!) is a well-known smell, exactly like Kotlin's !!.

The right alternatives are the narrowing tools you already know:

if (name !== null) return name.toUpperCase();   // narrow with a check
return (name ?? "guest").toUpperCase();          // default with ??
return name?.toUpperCase();                       // optional chaining — undefined if null

Each handles the null safely instead of asserting it away. Reach for ?., ??, or a narrowing check before you reach for ! — they deal with the null honestly, where ! just crosses your fingers.

When assertions are genuinely justified

Not never — a few honest uses, all with the same character as a justified any: you know something the compiler cannot, it is localised, and ideally commented.

  • The compiler cannot see a guarantee you have. A value you just checked but the compiler cannot track across a boundary (a narrowing it lost), or a DOM element you know exists because you control the HTML. Even then, a check or ?? throw is often cleaner.
  • A test asserting a precondition. const user = result as User in a test where the setup guarantees it, to fail loudly if wrong.
  • Const assertions (as const) are a different, safe use of as — they narrow to literals and add readonly, not override a type. as const is good; as SomeType to override is the one to be wary of.

The rule mirrors any: an assertion should be rare, deliberate, and justified by knowledge the compiler genuinely lacks — not a shortcut to silence an error. When you type as or !, pause and ask: can I narrow or validate instead? Usually you can, and it is safer.

Check your work

What as does. Overrides the compiler's inferred type for one expression — unchecked, no runtime conversion; a scoped promise like any.

Why as User on parsed data is wrong. It asserts the shape without checking it — a runtime crash if the data does not match; validate (narrow) instead.

Assert versus narrow. An assertion skips the check and trusts you; a guard checks and TypeScript trusts the check — prefer narrowing.

The double-assertion smell. as unknown as X forces through TypeScript's objection that the types are unrelated — a red flag, not a technique.

What ! does, and its danger. Removes null/undefined unchecked — reintroduces the null crash on your authority; a trail of ! is a smell.

The safe alternatives to !. ?. (optional chaining), ?? (default), or a narrowing check.

When an assertion is justified. Rarely — a guarantee the compiler cannot see, a test precondition, as const (which is a different, safe use) — deliberate and commented.

The question to ask. Before as or !, ask "can I narrow or validate instead?" — usually yes.

Practice

  1. Assert a getElementById result as HTMLInputElement and use .value. Then make the assertion wrong (assert a type it is not) and note there is no compile-time protection.
  2. Write JSON.parse(input) as User and crash it with bad data. Rewrite it with unknown + a guard.
  3. Trigger a "Conversion may be a mistake" error, then note that as unknown as silences it — and reason about why that is a smell.
  4. Use name!.toUpperCase() on a string | null and crash it with null. Rewrite three ways: ?., ??, and a narrowing check.
  5. Find a ! in code you can access and replace it with a safe alternative.
  6. Distinguish as const (safe, narrows to literals) from as SomeType (overrides — be wary).
  7. For three assertions, decide whether each is justified (compiler cannot know) or should be a narrow/ validate instead.

Official documentation

Next: making illegal states unrepresentable.

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