Non-null Assertion (!)
The non-null assertion operator is a postfix ! that tells TypeScript: "this value is definitely not null or undefined — remove those possibilities from the type." It is a compile-time escape hatch, not a runtime check. Use it only when you have external knowledge that TypeScript's type system cannot see.
The Problem It Solves
When strictNullChecks is enabled (the default in strict mode), TypeScript tracks null and undefined separately. Many APIs return T | null or T | undefined, requiring you to handle the absent case before using the value.
Sometimes you know a value cannot be null at a particular point — but TypeScript cannot prove it from the control flow alone.
// getElementById returns HTMLElement | null
const canvas = document.getElementById("myCanvas");
// canvas.getContext("2d"); // Error: canvas is possibly null
// With the ! operator — you assert it is not null
const canvas2 = document.getElementById("myCanvas")!;
const ctx = canvas2.getContext("2d"); // OK — no null in the type anymore! operator only affects the TypeScript type. At runtime, if the value really is null or undefined, you will get a TypeError. TypeScript trusts you completely when you use !.Syntax and Position
The ! is placed immediately after the expression whose null/undefined possibilities you want to remove.
// After a variable
let name: string | null = getName();
const upper = name!.toUpperCase();
// After a function call
const user = findUser(id)!;
// After property access
const value = map.get("key")!;
// Chained
const city = user!.address!.city!;
// After a generic function result
const first = arr.find((x) => x.id === 1)!;
// find returns T | undefined; ! removes undefinedWhen the ! Operator Is Appropriate
The non-null assertion is justified when you have external proof that TypeScript cannot verify:
DOM elements you control and know exist (e.g., an element added in your HTML template)
Values initialized in a class constructor via a lifecycle hook (e.g., Angular DI, test setup)
Array lookups where you have already validated the index manually
Map/Set retrievals immediately after a has() check that TypeScript cannot follow
After a regex match when you know the match succeeded
// Class with lazy initialization pattern
class ComponentBase {
private element!: HTMLElement; // initialized by init(), not constructor
init() {
this.element = document.getElementById("root")!;
}
render() {
this.element.innerHTML = "Hello"; // safe if init() was called first
}
}
// After a regex match
const match = "2024-01-15".match(/(d{4})-(d{2})-(d{2})/);
if (match) {
// match[1] is string | undefined without !
const year = match[1]!; // We know groups exist because pattern matched
const month = match[2]!;
const day = match[3]!;
}
// Map lookup after has()
const cache = new Map<string, number>();
if (cache.has("count")) {
const count = cache.get("count")!; // has() guarantees get() returns number
console.log(count.toFixed(0));
}! — TypeScript cannot yet reason that has followed by get guarantees a value. This is a known limitation of the standard library types.Definite Assignment Assertion
The ! also appears after a property name in a class to tell TypeScript the property will be definitely assigned before use, even though the constructor does not initialize it. This is called the definite assignment assertion.
class UserService {
// Without !: Error — property 'db' has no initializer and is not definitely assigned
private db!: Database;
private logger!: Logger;
constructor(private config: Config) {
// db and logger are injected by the DI framework after construction
}
// Called by the framework after DI injection
onInit() {
this.db = new Database(this.config.dbUrl);
this.logger = new Logger(this.config.logLevel);
}
async getUser(id: number) {
this.logger.info(`Fetching user ${id}`);
return this.db.findById(id);
}
}
// Also used for variables initialized in an external setup function
let server!: Server;
beforeAll(() => {
server = createServer(); // Jest/Vitest setup
});
afterAll(() => {
server.close();
});
test("GET /health", async () => {
const res = await request(server).get("/health");
expect(res.status).toBe(200);
});Safer Alternatives to !
Before reaching for !, consider whether a safer alternative fits:
Situation | Risky (uses !) | Safer alternative |
|---|---|---|
Maybe-null DOM element | el!.value | if (el) { el.value } |
Array element at index | arr[0]!.name | arr[0]?.name ?? "default" |
Map lookup after has() | map.get(k)! | map.get(k) ?? fallback |
Find result | arr.find(...)! | arr.find(...) ?? defaultValue |
Optional chaining result | obj?.prop! | Restructure to avoid nested optionals |
// Instead of: const name = user!.profile!.name!;
// Use optional chaining + nullish coalescing:
const name = user?.profile?.name ?? "Anonymous";
// Instead of: const el = document.getElementById("btn")!;
// Use a runtime check:
const el = document.getElementById("btn");
if (!el) throw new Error("Button not found in DOM");
el.click(); // TypeScript narrows to HTMLElement here
// Instead of: const first = items[0]!;
// Use optional chaining:
const first = items.at(0)?.id ?? -1;?.) and nullish coalescing (??) handle the majority of cases where developers are tempted to use !. They are safer because they provide a fallback rather than a crash.Combining ! with Optional Chaining
interface Config {
database?: {
host: string;
port?: number;
};
}
const cfg: Config = { database: { host: "localhost" } };
// Accessing a potentially optional chain
const port = cfg.database!.port ?? 5432;
// ^-- asserts database is defined (we set it above)
// Be careful: this crashes if database is actually undefined
// cfg = {}; // Then cfg.database! would throw at runtimeEslint and Linting Rules
Many teams restrict the use of ! via ESLint rules to enforce safer patterns:
// .eslintrc.js — common rules around non-null assertions
{
"@typescript-eslint/no-non-null-assertion": "warn",
// or "error" to ban entirely
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
// Bans: foo?.bar! (combining ?. and ! is almost always wrong)
}
// The rule catches:
const bad = user?.address!.city; // Error — optional chain already handles null
// If user is null, ?. short-circuits to undefined
// The ! after .address doesn't make it safe — it only changes the typeno-non-null-asserted-optional-chain catches a particularly common mistake: combining optional chaining and non-null assertion. The optional chain already produces undefined — the ! does not prevent the crash.Real-World Usage Pattern
// React + TypeScript: ref objects
import { useRef, useEffect } from "react";
function VideoPlayer() {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
// videoRef.current is HTMLVideoElement | null
// After mount, we KNOW it is set
videoRef.current!.play();
// ^ legitimate use — ref is always set after mount
}, []);
return <video ref={videoRef} src="/movie.mp4" />;
}
// More defensive version (preferred when possible)
function VideoPlayerSafe() {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (videoRef.current) {
videoRef.current.play();
}
}, []);
return <video ref={videoRef} src="/movie.mp4" />;
}Non-null Assertion vs. Optional Chaining — Decision Guide
Choosing between ! and ?. / ?? comes down to one question: do you want to crash loudly on null, or handle it gracefully?
Scenario | Use ! | Use ?. or ?? |
|---|---|---|
DOM element you own and always render | Yes | Overkill |
Optional user input that may be absent | No | Yes — provide a fallback |
Test fixture guaranteed by beforeAll() | Yes | Unnecessary |
API response field that could be null | No | Yes — handle the null case |
Class property set by DI framework | Yes (definite assignment) | Does not apply |
First element of a non-empty validated array | Acceptable | Also fine with .at(0) |
Migrating away from !
If you inherit a codebase with many ! operators scattered throughout, here is a pragmatic migration path:
Enable @typescript-eslint/no-non-null-assertion as a warning — see where they all are
Replace DOM lookups with instanceof guards or element queries typed with generics
Replace optional chaining results followed by ! with proper fallbacks via ??
For class properties, evaluate whether a different initialization pattern removes the need
Leave only the genuinely justified uses (DI-injected properties, known-present DOM nodes)
Promote the rule from warning to error once the count reaches zero
// Before migration: scattered ! operators
class App {
private router!: Router;
private store!: Store;
init(router: Router, store: Store) {
this.router = router;
this.store = store;
}
}
// After migration: constructor injection — no ! needed
class App {
constructor(
private router: Router,
private store: Store,
) {}
}! in Index Signatures and Optional Properties
Index signatures and optional properties both produce T | undefined. The ! operator can suppress the undefined in tightly controlled contexts.
const scores: Record<string, number> = { alice: 10, bob: 20 };
// scores["alice"] is number | undefined (index signature)
const aliceScore = scores["alice"]!; // number — we know the key exists
// More defensive:
const bobScore = scores["bob"] ?? 0; // number — safe fallback
// Optional property access
interface Profile {
bio?: string;
website?: string;
}
function formatBio(profile: Profile): string {
// bio is string | undefined
// Use ! only if we have already checked it exists elsewhere
if (profile.bio) {
return profile.bio.trim(); // TypeScript narrows naturally — no ! needed
}
return "";
}
// When you checked nullability earlier and TypeScript cannot see it
const profiles = [
{ bio: "Engineer", website: "https://example.com" },
].filter((p) => p.bio !== undefined);
// profiles[0].bio is still string | undefined from the interface
// Here ! is justified — filter guaranteed non-undefined
const bio = profiles[0].bio!.toUpperCase();! in Type-Level Code
The ! operator is exclusive to runtime expressions. At the type level, the equivalent operation is NonNullable<T>, which removes null and undefined from a type.
// Runtime: ! removes null/undefined from a value's type
const input = getInput()!; // asserts not null at runtime expression level
// Type level: NonNullable removes null/undefined from a type parameter
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// Useful in mapped types
type RequiredValues<T> = {
[K in keyof T]-?: NonNullable<T[K]>; // remove ? modifier AND null/undefined
};
interface Config {
host?: string | null;
port?: number | null;
}
type StrictConfig = RequiredValues<Config>;
// StrictConfig = { host: string; port: number } (required, non-null)NonNullable<T> in generic constraints and mapped types to express "this type, but without null or undefined." It is the type-level equivalent of the ! operator.Summary
! removes null and undefined from a type — purely at compile time, no runtime check
Use it only when you have external proof TypeScript cannot verify
Definite assignment ! on class properties tells the compiler about lifecycle-based initialization
Prefer optional chaining (?.) and nullish coalescing (??) for most null-handling needs
Never combine ?. and ! on the same chain — they work against each other
Enforce discipline with @typescript-eslint/no-non-null-assertion
Prefer constructor injection over DI-framework ! patterns when the framework allows it
NonNullable<T> is the type-level equivalent of ! — use it in mapped types and generic constraints
A misused ! converts a compile-time safety net into a runtime crash