I code in TypeScript all day. VS Code, React 18, Node, the usual mess. And that little exclamation mark? I’ve used it a lot. It’s fast. It feels bold. Sometimes it saves me. Sometimes it blows up my build at 2 a.m.
If you want the extended deep-dive with edge cases and benchmark numbers, it's all in my TypeScript exclamation mark deep dive where I document every pitfall I’ve hit in production.
You know what? It’s like a sharp knife. Cuts clean—until it cuts you.
For context, I only began caring about nullability after I migrated a hairy JavaScript codebase to TypeScript, so the habit runs deep.
What the “!” even does
There are two flavors:
- Non-null assertion: value!. It tells TypeScript, “Trust me, this isn’t null.”
- Definite assignment: name!: Type. It tells TypeScript, “I’ll set this later. Promise.”
Short. Loud. Bossy.
It first showed up back in TypeScript 2.0’s release notes if you’re curious about the official rationale.
Real code I wrote (and shipped)
A React ref that needed focus
This was a login screen. Pretty simple.
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current!.focus(); // I told TS: it's there
}, []);
And yes, the day I shipped this I actually forgot the type parameter in another component—TypeScript politely looked the other way until runtime reminded me.
This worked fine. In my app, the input does exist after mount. It feels safe here. Still, Strict Mode runs effects twice, so I saw two blinks. Not a big deal, but worth saying.
DOM query where I got cocky
I did this on a landing page:
const saveBtn = document.querySelector('#save')!;
saveBtn.addEventListener('click', () => {
console.log('saved!');
});
On my machine it was perfect. On a slow phone, it crashed. The script ran before the button showed up. Boom: “Cannot read properties of null.” I got a bug report on Safari. Felt dumb.
Class field I said I’d set “later”
class Store {
private db!: Database;
async init() {
this.db = await connect();
}
getUser(id: string) {
return this.db.get(id); // crash city if init() is missed
}
}
I forgot to call init() in one path. It blew up in QA. I knew better. But a busy day is a busy day.
When “!” actually helped me
- I had to ship a tiny fix fast. The type was noisy. I knew the value was set by the time the code ran. One “!” and done.
- In unit tests, after I create the DOM or mock data, I use “!” to keep tests short.
- In my React entry file (main.tsx), I know the HTML root exists. I write a comment and use it.
// index.html always has #root (build step ensures it)
const root = document.getElementById('root')!;
createRoot(root).render(<App />);
That same 'I know it’s there' attitude is sometimes OK for infrastructure code too; for instance I recently dug into how to validate a file-path argument in a TypeScript CLI without sprinkling ! everywhere.
Clear claim. Clear risk. I accept it there.
Where it bit me, hard
- Slow network = null at runtime. My fearless “!” turned into a crash.
- A coworker moved a script tag. My “safe” DOM code wasn’t safe anymore.
- I merged a refactor and forgot init(). The class field with “!” hid the risk from TypeScript, and it didn’t warn me.
Honestly, the worst part was that TypeScript tried to help. I told it to hush.
Safer moves I reach for now
1) Narrow first, then use it
const btn = document.querySelector('#save');
if (!btn) throw new Error('Missing #save button'); // loud but honest
btn.addEventListener('click', onSave);
Now if markup changes, I get a clean error. Not a weird null crash.
2) Optional chaining and friends
const name = user?.profile?.name ?? 'Guest';
No “!” needed. No surprise crash.
While we’re on safer syntax, my hot take on TypeScript’s forEach loop shows how even iterating can be less foot-gun-y when you lean on the compiler.
3) A tiny assert helper
I use this a lot:
function assertExists<T>(value: T | null | undefined, msg?: string): asserts value is T {
if (value == null) throw new Error(msg ?? 'Expected value to exist');
}
const el = document.querySelector('#save');
assertExists(el, '#save not found');
el.addEventListener('click', onSave);
Now TypeScript narrows the type. And I get a clear message if it’s missing.
4) Constructor setup beats “I’ll set it later”
class Store {
constructor(private db: Database) {}
getUser(id: string) {
return this.db.get(id);
}
}
Sticking to constructor injection also plays nicely with large refactors—renaming a TypeScript field while preserving JSDoc is way easier when the compiler can trace every assignment.
No “!” at all. Fewer headaches.
5) React ref without bravado
if (inputRef.current) {
inputRef.current.focus();
}
It’s one extra line. It saves future me.
For an even deeper dive into writing safer TypeScript code, check out the practical guides on Improving Code.
My quick guardrails
- If I add “!”, I leave a comment: why it’s safe.
- I keep strictNullChecks on. I like the noise.
- I use @typescript-eslint/no-non-null-assertion.
- I turned on noUncheckedIndexedAccess. It nudges me to handle missing items.
- I ask: can I prove this with code? If yes, I skip “!”.
For linting support, the rule @typescript-eslint/no-non-null-assertion is what keeps me honest when I’m tempted to sprinkle exclamation points everywhere.
For a battle-tested approach to inline docs, my walkthrough on writing maintainable comments in TypeScript shows the exact patterns I copy-paste.
When I still use “!” without guilt
- In tests, after I set up the DOM or mocks.
- In main.tsx, right on the root element, with a comment.
- When a framework guarantees a node (like a portal target) and it’s part of the build, not user input.
That’s it. Very small list.
Tiny cheatsheet
- Non-null “!”: value must not be null at runtime, or you crash.
- Definite “!” on fields: you promise to assign before use. If you forget, boom.
- Better tools first: narrowing, optional chaining, assert functions, constructors.
Verdict
I give the TypeScript “!” a 3.5 out of 5.
It’s fast. It’s bold. It’s also a little reckless. Use it like a power tool—only when you can name the guard and point to the fence. And leave a note for the next person. Which might be you.
Just as you should weigh the real-world trade-offs before slapping an exclamation mark into your codebase, you might want to do a bit of homework before you hit “download” on yet another dating app. If you’re currently deciding where to invest your swipes, this no-fluff Hinge review breaks down the app’s core features, hidden costs, and success tips so you can judge whether it’s worth your time.
