Type Assertions & Non-null
Reviewed & published by Brayan K
A type assertion (value as Type) overrides the compiler's inferred type with one you supply, while the non-null assertion (value!) removes null and undefined — both are compile-time only and never change the value at runtime.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Use 'as' to tell the compiler a value's real type
- Understand that assertions are erased at runtime (no conversion)
- Pin literals and lock immutability with 'as const'
- Strip null/undefined safely with the non-null assertion '!'
- Recognise the angle-bracket form and why .tsx avoids it
- Use the 'as unknown as T' escape hatch — and when NOT to
💡 Real-World Analogy
A type assertion is like writing your own name badge at a conference. The badge says "Senior Engineer" because you typed it — the system never verified it. If it's true, great, everyone treats you correctly. If it's a lie, nothing stops you walking around with the wrong badge until something goes wrong. The as keyword is you printing the badge; the runtime is the conference that never checks it.
1. The as Assertion: Overriding the Type
Sometimes you know more than the compiler. When a value comes back as unknown from JSON.parse, or the DOM hands you a generic Element | null, you can use as to re-label it as the precise type you expect. Crucially, this is a promise to the compiler, not a conversion — the underlying value is untouched. The older angle-bracket form <Type>value means exactly the same thing, but .tsx files avoid it because it clashes with JSX tags.
// 'as' re-labels a value's type. Two common, SAFE shapes:
// 1) Narrowing a loose 'any' / 'unknown' you genuinely know:
const data: unknown = JSON.parse('{"name":"Ada"}');
const user = data as { name: string };
console.log(user.name); // ✅ allowed — you promised the shape
// 2) Telling the DOM API a concrete element type:
const input = document.getElementById("email") as HTMLInputElement;
input.value = "hi"; // ✅ .value only exists on HTMLInputElement
// Angle-bracket form means the SAME thing (avoid it in .tsx — clashes with JSX):
const x = <number>someValue; // identical to: someValue as number
// ❌ TypeScript blocks assertions between clearly unrelated types:
// const bad = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake...// A type ASSERTION (written 'value as Type') tells the compiler:
// "trust me, I know this value is really this Type." It changes NOTHING
// at runtime — it only changes what the type checker believes.
// In TypeScript: const el = document.getElementById("x") as HTMLInputElement;
// We'll simulate it: a value typed loosely that we KNOW is more specific.
const data = JSON.parse('{"name":"Ada","age":36}'); // typed 'any' in TS
// In TS we'd assert the shape: const user = data as { name: string; age: number };
const user = data; // at runtime, just the object
console.log(user.name); // Ada
console.log(user.age + 1); // 37
// Assertions do NOT convert. They never run code, never coerce:
const n = "42"; // a string at runtime
// In TS: const bad = n as number; // compiles, but it's STILL a string!
console.log(typeof n); // string (assertion would NOT change this)
console.log(n + 1); // 421 (string concat — the lie bites you)2. as const and the Non-null !
Two special assertions earn their own spotlight. as const tells TypeScript to infer the narrowest possible literal type and make everything deeply readonly — perfect for fixed config and tuple-of-literals menus. The non-null assertion ! strips null and undefined from a type, turning Element | null into Element. Both are type-only: if the value really is null at runtime, ! will not save you.
// 'as const' pins literals + makes things readonly:
let dir = "up" as const; // type is the literal "up" (not string)
const sizes = ["sm", "md", "lg"] as const;
// type: readonly ["sm", "md", "lg"] — and sizes.push("xl") is now an error
// The non-null assertion '!' removes 'null | undefined' from a type:
const el = document.getElementById("app")!; // type Element, not Element | null
el.classList.add("ready"); // ✅ no "possibly null" complaint
// '!' is a TYPE-only promise. If you're wrong, it crashes at runtime —
// prefer a real check when you cannot be 100% sure:
const found = sizes.find(s => s === "md"); // string | undefined
if (found) console.log(found.toUpperCase()); // ✅ safe narrowing, no '!'// 'as const' freezes a value to its NARROWEST literal type AND makes it
// deeply readonly. In TS: const dir = "up" as const; // type is "up", not string
// At runtime 'as const' adds nothing — but we can show the BEHAVIOUR it locks in.
const point = { x: 1, y: 2 }; // in TS without 'as const', x/y are 'number'
// With 'as const' TS would type it { readonly x: 1; readonly y: 2 }.
// A tuple of literals is the classic use — a fixed menu:
const sizes = ["sm", "md", "lg"]; // 'as const' -> readonly ["sm","md","lg"]
console.log(sizes[0]); // sm
console.log(sizes.length); // 3
// The non-null assertion '!' says "this is NOT null/undefined, trust me":
// const first = sizes.find(s => s === "md")!; // type string, not string | undefined
const found = sizes.find(s => s === "md"); // at runtime may be undefined
console.log(found); // md
// '!' only removes null/undefined from the TYPE; runtime is unchanged:
const maybe = null;
// const crash = maybe!.length; // TS allows it, but runtime would CRASH
console.log(maybe ?? "was null"); // was null3. The as unknown as T Escape Hatch
TypeScript deliberately blocks a direct assertion between two types it considers unrelated, because that's usually a bug. When you genuinely need to bridge such a gap — wrapping an untyped library, say — you route through unknown: value as unknown as T. This double assertion silences the compiler entirely, which is exactly why it's a last resort: you've switched off the safety net for that value.
// Direct assertion between unrelated types is blocked:
const s = "1007";
// const n = s as number; // ❌ may be a mistake...
// The escape hatch — go through 'unknown' to force it:
const n = s as unknown as number; // compiles, but n is STILL "1007" at runtime!
// This is a last resort. Use it ONLY when you truly know better than TS
// (e.g. bridging an untyped library). It turns OFF the safety net, so the
// far better fix is almost always a real conversion or a type guard:
const safe = Number(s); // genuine runtime number// The DOUBLE assertion 'value as unknown as T' is the escape hatch.
// TS blocks a direct 'as' between two unrelated types. Going via 'unknown'
// silences it: "forget what you knew, now treat it as T." DANGEROUS but real.
// In TS: const id = (someString as unknown as number);
// Simulate: we have a "stringy id" we forcibly treat as a number-shaped thing.
const raw = "1007";
// In TS: const id = raw as unknown as number; // compiles; runtime still a string!
const id = raw;
console.log(typeof id); // string (no runtime conversion happened)
// The SAFE alternative is a real runtime conversion, not an assertion:
const realNum = Number(raw);
console.log(typeof realNum); // number
console.log(realNum * 2); // 2014
// Rule: an assertion is a PROMISE to the compiler. If the promise is false,
// the bug shows up at runtime where TS can no longer help you.🎯 Your Turn
Here's a typical assertion job: a config object parsed from JSON that you read with a known shape. Fill in the two blanks marked ___, then run it.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// In TS this object would be typed loosely; we ASSERT a precise shape and read it.
const config = JSON.parse('{"theme":"dark","retries":3}');
// 1) Read the theme. In TS we'd write: (config as { theme: string }).theme
const theme = config.___; // 👉 the property name: theme
// 2) Use the number. In TS: (config as { retries: number }).retries
const total = config.retries + ___; // 👉 add the number 1
console.log("Theme: " + theme); // Theme: dark
console.log("Attempts: " + total); // Attempts: 4
// ✅ Expected output:
// Theme: dark
// Attempts: 4Common Errors (and the fix)
- ❌ "Conversion of type 'string' to type 'number' may be a mistake" — you asserted between unrelated types. ✅ If you really must, go via as unknown as number; far better, do a real conversion with Number(x).
- ❌ Trusting "42" as number to give you maths — it's still a string at runtime, so + 1 concatenates. ✅ Assertions don't convert; call Number() / parseInt() for actual numbers.
- ❌ Using ! on something that really is null — TS stays quiet but you crash with "Cannot read properties of null". ✅ Replace ! with if (value) whenever you aren't 100% sure.
- ❌ Angle-bracket <T>value in a .tsx file — the parser reads it as a JSX tag and errors. ✅ Always use value as T in React/TSX code.
Pro Tips
- 💡 Prefer narrowing over asserting. A typeof check or type guard proves the type to both you and the runtime; as only convinces the compiler.
- 💡 Use as const for config and lookup tables so values stay literal ("GET" | "POST") instead of widening to string.
- 💡 Treat every ! as a code-review flag. Each one is a place you've promised non-null with no runtime guarantee.
- 💡 Comment every double assertion. as unknown as T turns off type safety — leave a note saying why it's justified.
📚 Keep learning
- TypeScript Handbook — Type Assertions
- TypeScript Docs — const assertions (as const)
- TypeScript Handbook — Non-null Assertion Operator
Mini-Challenge: Safe DOM-style Lookup
No blanks this time — just a brief and a starting outline. Build the lookup yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: a safe DOM-style assertion, simulated.
// In TS, document.querySelector returns Element | null, so people write:
// const input = document.querySelector("#email") as HTMLInputElement;
// then read input.value. Let's model that pattern with plain objects.
//
// 1. Given the fake DOM below, write getValue(selector) that:
// - looks up dom[selector] (this is the "as HTMLInputElement" target)
// - returns its .value, or "(not found)" if missing
// 2. Call it for "#email" and for "#missing".
//
// ✅ Expected output:
// ada@site.com
// (not found)
const dom = { "#email": { value: "ada@site.com" } };
// your code here🎉 Lesson Complete
- ✅ value as Type re-labels a value's type and is erased at runtime
- ✅ Assertions do not convert — "42" as number is still a string
- ✅ as const pins literals and makes values deeply readonly
- ✅ The non-null ! strips null | undefined from a type only
- ✅ as unknown as T is the escape hatch — a true last resort
Practice quiz
What does a type assertion 'value as Type' do?
- Converts the value at runtime
- Parses strings into numbers
- Tells the compiler to treat the value as Type; nothing changes at runtime
- Validates the value
Answer: Tells the compiler to treat the value as Type; nothing changes at runtime. An assertion only changes what the type checker believes; the value is untouched.
What is the result of '"42" as number' followed by + 1 at runtime?
- '421' (string concatenation, still a string)
- 43 (a number)
- NaN
- a compile error
Answer: '421' (string concatenation, still a string). The value is still the string '42', so '42' + 1 is '421'. Assertions don't convert.
Why is the angle-bracket form <Type>value avoided in .tsx files?
- It is slower
- It only works for numbers
- It is deprecated everywhere
- It clashes with JSX tag syntax
Answer: It clashes with JSX tag syntax. In .tsx the parser reads <Type> as a JSX tag, so use 'value as Type' instead.
What does 'as const' do?
- Freezes at runtime
- Infers the narrowest literal type and makes the value deeply readonly
- Converts to a constant function
- Removes null
Answer: Infers the narrowest literal type and makes the value deeply readonly. as const pins literals (e.g. 'up' stays 'up') and makes everything deeply readonly in the types.
What does the non-null assertion '!' do?
- Removes null and undefined from a value's type (type-only)
- Throws if the value is null
- Converts null to a default
- Negates a boolean
Answer: Removes null and undefined from a value's type (type-only). el!.value tells TypeScript it isn't null here, but it doesn't protect you at runtime.
What happens if you use '!' on a value that really is null at runtime?
- TypeScript blocks it
- It returns undefined
- It crashes - '!' is type-only and offers no runtime guarantee
- It returns 0
Answer: It crashes - '!' is type-only and offers no runtime guarantee. The non-null assertion is a promise; if wrong, you get a runtime crash.
Why does TypeScript block 'someString as number' directly?
- number is a reserved word
- string and number are unrelated, so it is likely a mistake
- as cannot target primitives
- it never blocks this
Answer: string and number are unrelated, so it is likely a mistake. TS rejects assertions between clearly unrelated types as a probable bug.
What is the 'as unknown as T' double assertion for?
- A safe conversion
- Improving inference
- Freezing a value
- An escape hatch to force an assertion between unrelated types - a last resort
Answer: An escape hatch to force an assertion between unrelated types - a last resort. Routing through unknown silences the checker; it disables type safety for that value.
What is the safer alternative to asserting a string to a number?
- as number
- A real conversion like Number(str)
- as unknown as number
- !str
Answer: A real conversion like Number(str). Number(str) actually produces a numeric value at runtime, unlike an assertion.
Compared with an assertion, what does narrowing (e.g. if (value)) give you?
- Nothing extra
- Faster code only
- Proof of the type to both the compiler AND the runtime
- A widened type
Answer: Proof of the type to both the compiler AND the runtime. A typeof/if check protects at runtime too; 'as' only convinces the compiler.
Continue this course
- Previous: Checkpoint: Advanced Types
- Next: any vs unknown vs never — The three special types — escape hatch, safe unknown, and impossible never
- Quick reference: TypeScript cheat sheet
Frequently asked questions
Does 'as' convert or change the value at runtime?
No. A type assertion is purely a compile-time instruction to the type checker — it is erased before the code runs. 'value as number' does not parse, coerce, or round anything; if the value was a string at runtime, it stays a string. For a real conversion you need actual code like Number(value) or String(value).
When is a type assertion safe versus dangerous?
It is reasonably safe when you genuinely have more information than the compiler — for example narrowing an 'unknown' you just validated, or telling the DOM that getElementById returned an HTMLInputElement you control. It is dangerous whenever you are guessing, because an assertion silences the very error that would have caught your mistake, pushing the bug to runtime.
What does the non-null assertion '!' do?
Appending '!' to an expression removes 'null' and 'undefined' from its type — el!.value tells TypeScript 'this is definitely not null here.' Like 'as', it only affects the type, not the runtime. If the value really is null at runtime you'll get a crash, so prefer an explicit if-check when you cannot be certain.
Why would I ever write 'value as unknown as T'?
TypeScript refuses a direct assertion between two types it considers unrelated. Routing through 'unknown' (a double assertion) forces the conversion through. It is an escape hatch for bridging untyped or mistyped code, and it completely disables type safety for that value — so treat it as a last resort and add a comment explaining why it's needed.