string | number | Date) into a narrower, more specific type as your code executes. TypeScript’s Control Flow Analyzer automatically tracks runtime checks across if, else, switch, return, and throw statements.1. What is Control Flow Analysis (CFA)?#
In TypeScript, type checking is not static line-by-line validation. The compiler employs Control Flow Analysis (CFA)—it constructs a flow graph of your code, tracking variable assignments, branch conditions, and early returns.
As execution moves down a function branch, TypeScript continuously refines the variable’s type based on logic guards:
flowchart TD
Start["Variable x: string | number"] --> Guard{"typeof x === 'string'"}
Guard -->|True| BranchString["Inferred Type: string\nCall .toUpperCase()"]
Guard -->|False| BranchNumber["Inferred Type: number\nCall .toFixed()"]
2. Guard 1: typeof Guards#
The typeof operator checks JavaScript primitive types at runtime.
Valid typeof return strings recognized by TypeScript’s type checker:
"string""number""bigint""boolean""symbol""undefined""object""function"
function formatInput(input: string | number | boolean): string {
if (typeof input === "string") {
// Inferred: string
return input.trim().toUpperCase();
}
if (typeof input === "number") {
// Inferred: number
return `$${input.toFixed(2)}`;
}
// Inferred: boolean
return input ? "ENABLED" : "DISABLED";
}The typeof null === "object" Trap#
In JavaScript, typeof null evaluates to "object" due to a historical bug in the original 1995 JS engine implementation.
Because of this, using typeof x === "object" to check for objects will accidentally allow null through:
function processObject(data: { name: string } | null) {
if (typeof data === "object") {
// ❌ DANGER: 'data' is inferred as '{ name: string } | null'!
// console.log(data.name); // Error: 'data' is possibly 'null'.
}
// 🟢 CORRECT: Always check for non-null explicitly!
if (typeof data === "object" && data !== null) {
// Inferred: { name: string }
console.log(data.name); // Safe!
}
}3. Guard 2: instanceof Guards#
The instanceof operator tests whether an object’s prototype chain contains the .prototype property of a constructor function or Class.
It is used to narrow instances of classes (e.g., Date, Error, RegExp, Array, or custom classes):
function parseDateInput(input: string | Date | Error): string {
if (input instanceof Date) {
// Inferred: Date
return input.toISOString();
}
if (input instanceof Error) {
// Inferred: Error
return `[ERROR]: ${input.message}`;
}
// Inferred: string
return input.toLowerCase();
}instanceof can fail across cross-realm boundaries (such as window.frames or iframe contexts), because Date in frame A has a different prototype reference than Date in frame B.
4. Guard 3: The in Operator Guard#
The in operator checks if a specific property key exists in an object or its prototype chain. It is ideal for distinguishing between different object interfaces:
interface AdminAccount {
id: string;
adminPermissions: string[];
}
interface UserAccount {
id: string;
email: string;
}
function processAccount(account: AdminAccount | UserAccount) {
if ("adminPermissions" in account) {
// Inferred: AdminAccount
console.log(`Admin with ${account.adminPermissions.length} permissions`);
} else {
// Inferred: UserAccount
console.log(`User email: ${account.email}`);
}
}5. Guard 4: Equality Narrowing (===, !==)#
TypeScript uses literal equality checks (===, !==, ==, !=) to narrow types, including primitive literal values and discriminant keys:
function compareValues(a: string | number, b: string | boolean) {
if (a === b) {
// Because 'a' equals 'b', 'a' and 'b' MUST both be 'string'!
console.log(a.toUpperCase());
console.log(b.toUpperCase());
} else {
console.log(a); // Inferred: string | number
console.log(b); // Inferred: string | boolean
}
}Checking for null or undefined (== null)#
Using loose equality val == null checks for both null and undefined simultaneously:
function handleValue(val: string | null | undefined) {
if (val == null) {
// Inferred: null | undefined
return "Default Value";
}
// Inferred: string
return val.toUpperCase();
}6. Truthiness Narrowing & Falsy Pitfalls#
TypeScript narrows types inside if (val) condition blocks by stripping null, undefined, 0, "", NaN, and false from the type union:
function printString(str: string | null) {
if (str) {
// Inferred: string
console.log(str.toUpperCase());
}
}The Falsy Value Bug#
Be extremely cautious when performing truthiness narrowing on string or number types, as empty strings "" and the number 0 evaluate to false at runtime:
function printCount(count: number | null | undefined) {
// ❌ BUG: If count is 0, (0) evaluates to false, skipping this block!
if (count) {
console.log(`Count is: ${count}`);
}
}
// 🟢 CORRECT: Explicitly check for null and undefined!
function printCountCorrect(count: number | null | undefined) {
if (count !== null && count !== undefined) {
console.log(`Count is: ${count}`);
}
}
printCountCorrect(0); // Outputs: "Count is: 0"
Summary & Next Steps#
In this episode:
- We unpacked Control Flow Analysis (CFA) and how TS tracks code execution branches.
- We analyzed
typeofguards and avoided thetypeof null === "object"trap. - We used
instanceoffor class instances andinfor object property existence checks. - We demonstrated equality narrowing (
===) and truthiness guard pitfalls (0and"").
In Episode 10: Custom Type Guards (is predicate), we will learn how to write reusable boolean functions that teach TypeScript how to narrow complex types!

