Nullability, Non-Null Assertions, and Type Assertions

Introduction

null and undefined represent missing values—but they are a common source of runtime errors. With strictNullChecks, TypeScript forces you to handle absence explicitly. When you know more than the compiler, assertions (as, !) can help—but overuse hides bugs. This chapter covers safe null handling and when assertions are appropriate.

Prerequisites

Nullable Types

typescript
let title: string | null = null;
 
function setTitle(value: string | null): void {
  title = value;
}

Optional properties are similar:

typescript
interface User {
  name: string;
  nickname?: string;
}
 
const u: User = { name: "Ada" };
// u.nickname is string | undefined

strictNullChecks in Practice

Without strict null checks, null could flow anywhere. With strict mode:

typescript
function length(name: string): number {
  return name.length;
}
 
const maybe: string | null = null;
// length(maybe);  // Error: null not assignable to string

Fix by narrowing or branching:

typescript
function safeLength(name: string | null): number {
  if (name === null) {
    return 0;
  }
  return name.length;
}

Optional Chaining and Nullish Coalescing

typescript
interface Profile {
  user?: {
    email?: string;
  };
}
 
function emailHint(profile: Profile): string {
  const email = profile.user?.email;
  return email ?? "no-email@placeholder.local";
}

Code explanation:

  • ?. stops evaluation and returns undefined if a step is null/undefined
  • ?? replaces only null or undefined (not 0 or "")

Non-Null Assertion Operator !

Tell the compiler a value is not null or undefined:

typescript
function process(value: string | null): void {
  if (value === null) {
    return;
  }
  console.log(value!.length);
}

After a null check, ! is usually redundant—prefer the narrowed value without !.

Warning

Do Not Use ! to Silence Errors

document.getElementById("app")! compiles but can crash if the element is missing. Validate or narrow instead.

Legitimate rare use: values guaranteed by framework lifecycle when types are too loose.

Type Assertions with as

Override the compiler’s inferred type when you have proof:

typescript
const input = document.getElementById("email") as HTMLInputElement;
const value = input.value;

Code explanation:

  • getElementById returns HTMLElement | null in strict DOM typings
  • as HTMLInputElement asserts element type—you should still check for null

Assertion vs narrowing

ApproachChecked by compilerRuntime safe if wrong
Narrowing (if, guards)YesSafer
as assertionNoCan crash
! non-null assertionNoCan crash

Prefer narrowing for user input and external data.

Angle Bracket Syntax (Legacy)

typescript
const el = <HTMLInputElement>document.getElementById("email");

Equivalent to as in many cases. Avoid in .tsx files—JSX parses < as tags. Use as in React/TSX projects.

Asserting to unknown First

Safer migration from untyped data:

typescript
function parseConfig(raw: unknown): { port: number } {
  if (typeof raw !== "object" || raw === null) {
    throw new Error("Invalid config");
  }
  const obj = raw as Record<string, unknown>;
  if (typeof obj.port !== "number") {
    throw new Error("Invalid port");
  }
  return { port: obj.port };
}

Even better: use a schema validator (Zod, etc.) in production apps—outside this chapter’s scope.

definite Assignment Assertion !

For class fields initialized outside the constructor path TypeScript cannot see:

typescript
class Logger {
  private stream!: NodeJS.WritableStream;
 
  init(): void {
    this.stream = process.stdout;
  }
 
  write(message: string): void {
    this.stream.write(message);
  }
}

! on the field says “I will assign before use.” Misuse causes runtime errors.

Practical Example: DOM Helper

Runtime check + type predicate—no blind as.

Style Guide

  1. Model absence with | null / | undefined / ? explicitly.
  2. Narrow at boundaries (API, DOM, JSON.parse).
  3. Avoid ! in application logic unless documented invariant.
  4. Use as for DOM casts and interop; validate when failure is possible.
  5. Never chain as any to bypass errors.

FAQ

undefined vs null?

JavaScript uses both. TypeScript treats them separately unless you use ?? or optional types. Pick a team convention (often undefined for optional, null for intentional absence).

Is strictNullChecks required?

Strongly recommended. Disabling it weakens the main benefit of TypeScript for app code.

Can I assert to a union?

Yes: value as string | number—still no runtime check.

What about optional parameters vs null?

name?: string means string | undefined. name: string | null allows explicit null. Be consistent in public APIs.

Does ! affect runtime?

No. It is erased—purely compile-time.

When is satisfies better than as?

When you want inference plus validation against a type (TypeScript 4.9+). Use for config objects; covered in advanced material.