Lesson 74 of 80
The `!` you put after a class field or variable name — not the one after an expression — tells the compiler 'this will be set before anyone reads it,' for the cases where control flow analysis genuinely can't see that for itself.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
strictPropertyInitialization (part of strict: true) requires every class field to either have an initializer, be assigned in the constructor, or be typed to include undefined. That's a good default — an uninitialized field really can be undefined at runtime, and the compiler is right to want proof otherwise.
But sometimes you have proof the compiler can't see: a field genuinely gets set, just not by the constructor directly — maybe by a decorator, a dependency-injection framework, an init() method called right after construction, or a test's beforeEach. The definite assignment assertion (!, placed right after the field or variable name, not after an expression) tells the compiler "trust me, this will be assigned before it's read" and turns off the check for that one declaration.
class ChartWidget {
// Set by connectedCanvas(), called immediately after construction —
// the compiler has no way to know that from the constructor alone.
canvas!: HTMLCanvasElement;
connectedCanvas(el: HTMLCanvasElement) {
this.canvas = el;
}
draw() {
const ctx = this.canvas.getContext("2d"); // no error — canvas is trusted non-undefined
}
}
const widget = new ChartWidget();
widget.connectedCanvas(document.querySelector("canvas")!);
widget.draw();
Without the ! after canvas, this file fails to compile: strictPropertyInitialization sees a field with no initializer and no constructor assignment, and refuses to let draw() treat this.canvas as definitely an HTMLCanvasElement. The ! is a promise about the field, made once at the declaration site — different from the non-null assertion operator, which is a promise about one expression, made at each read site.
The same syntax works on top-level let variables, for the same reason:
let config: AppConfig;
function loadConfig() {
config = readConfigFile(); // the compiler sees this assignment
}
loadConfig();
console.log(config.env); // ❌ still flagged — TS can't prove loadConfig() ran before this line
let config2!: AppConfig;
loadConfig2();
console.log(config2.env); // ✅ trusted
canvas genuinely never gets assigned — say connectedCanvas() is never called — this.canvas.getContext throws a real TypeError at runtime, exactly as if the ! weren't there. TypeScript trusted you; it didn't verify you.canvas!: HTMLCanvasElement doesn't make canvas optional or nullable — reads of this.canvas are still typed as plain HTMLCanvasElement, not HTMLCanvasElement | undefined. That's the entire point: you're asserting definite presence, not admitting possible absence.! specifically when the assignment genuinely happens outside code the compiler can trace — framework-managed fields, decorators, DI containers — not as a shortcut past a real "I forgot to initialize this" bug.This is the declaration-site counterpart to the non-null assertion operator (expr!) covered in the strictNullChecks lesson — both are ways of telling the compiler "I have information you don't," and both share the same risk: the compiler stops checking, but the runtime doesn't stop enforcing. If you understand why user!.name can still throw, you already understand why an unassigned canvas! field can too.