Conditionals
TypeScript inherits JavaScript’s conditional surface: if/else if/else, the ternary operator (?:), the nullish coalescing operator (??), the optional chaining operator (?.), and the logical-assignment operators (&&=, ||=, ??=). The conditional condition is JavaScript’s truthy/falsy — false, 0, "", null, undefined, NaN are falsy; everything else is truthy. The TypeScript additions are principally type-driven: the compiler narrows types within branches based on the condition, admitting substantial type-safe discrimination. The combination — full JavaScript control flow plus TypeScript’s narrowing — is the substance of the conditional surface.
if / else if / else
The principal form:
if (condition) {
// body
} else if (other) {
// body
} else {
// body
}
Examples:
if (x > 0) {
console.log("positive");
} else if (x < 0) {
console.log("negative");
} else {
console.log("zero");
}
The braces are not required for single-statement bodies (unlike Go and Rust):
if (cond) doSomething(); // OK; single statement
if (cond) doSomething();
else doOtherThing();
The conventional discipline is to use braces consistently; many style guides require them.
Truthiness
JavaScript admits truthiness coercion. Falsy values:
false0,-0,0n(BigInt zero)""(empty string)nullundefinedNaN
Everything else is truthy:
if ("hello") { /* runs */ }
if (1) { /* runs */ }
if ([]) { /* runs (empty array is truthy!) */ }
if ({}) { /* runs (empty object is truthy!) */ }
if ("") { /* skipped */ }
if (0) { /* skipped */ }
if (null) { /* skipped */ }
The empty array and empty object pitfalls are conventional gotchas; the conventional defences:
if (items.length > 0) { /* ... */ }
if (Object.keys(obj).length > 0) { /* ... */ }
Type narrowing
Within an if, the compiler narrows the type of the condition’s variables:
function process(x: string | number) {
if (typeof x === "string") {
x.toUpperCase(); // x is narrowed to string
} else {
x.toFixed(2); // x is narrowed to number
}
}
function nullable(s: string | null) {
if (s !== null) {
return s.toUpperCase(); // narrowed to string
}
return "default";
}
function checkArray(items: number[] | undefined) {
if (items) {
return items.reduce((a, b) => a + b, 0); // narrowed to number[]
}
return 0;
}
Treated in Narrowing.
The ternary operator
The form condition ? a : b admits a value-returning conditional:
const max = a > b ? a : b;
const status = age >= 18 ? "adult" : "minor";
const greeting = name ? `Hello, ${name}` : "Hello, stranger";
The ternary is right-associative — chained ternaries read left-to-right:
const category =
age < 13 ? "child" :
age < 20 ? "teen" :
age < 65 ? "adult" :
"senior";
The conventional discipline avoids deeply nested ternaries; explicit if/else if/else is conventionally clearer.
Nullish coalescing ??
The ?? operator returns the right operand only if the left is null or undefined:
const port = config.port ?? 8080; // 8080 if port is null/undefined
const name = user.name ?? "anonymous";
// Distinct from || (treats all falsy as "missing"):
const a = 0 || 10; // 10 (0 is falsy)
const b = 0 ?? 10; // 0 (0 is not null/undefined)
const c = "" || "default"; // "default"
const d = "" ?? "default"; // ""
The conventional discipline uses ?? over || for default-value patterns where 0 and "" are valid.
Optional chaining ?.
The ?. admits “access this if the receiver is not null/undefined”:
const name = user?.profile?.name; // undefined if any link is missing
const first = arr?.[0]; // index access
const result = fn?.(arg); // function call
// Chained:
const city = user?.address?.city ?? "unknown";
The mechanism short-circuits to undefined if any link in the chain is null or undefined.
A subtle pitfall: ?. does not short-circuit on subsequent operations:
const length = obj?.items?.length; // OK; undefined if items missing
// But assignment short-circuits the whole expression:
obj?.items?.push(x); // safe; no-op if items missing
Logical assignment
The logical-assignment operators (ES2021):
x &&= y; // x = x && y
x ||= y; // x = x || y
x ??= y; // x = x ?? y
The conventional uses are “set if missing”:
config.timeout ??= 30; // assign only if null/undefined
items[key] ||= []; // assign empty array if not set
// "Update if exists":
user.profile &&= updateProfile(user.profile);
Common patterns
Early return
function process(input: string | undefined): string {
if (!input) {
return "default";
}
if (input.length > 100) {
return "too long";
}
return input.toUpperCase();
}
The pattern reduces nesting; the conventional TypeScript style favours early returns.
Null check
function getName(user: User | null): string {
if (user === null) {
return "anonymous";
}
return user.name;
}
// Or with optional chaining and ??:
function getName(user: User | null): string {
return user?.name ?? "anonymous";
}
Validate-and-process
function processInput(input: unknown): string {
if (typeof input !== "string") {
throw new Error("expected a string");
}
if (input.trim() === "") {
throw new Error("input is empty");
}
return input.trim().toUpperCase();
}
Default parameters
function greet(name?: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name ?? "world"}!`;
}
// Or with default values in parameters:
function greet(name = "world", greeting = "Hello"): string {
return `${greeting}, ${name}!`;
}
Conditional spread
const config = {
host: "localhost",
port: 8080,
...(useSSL && { ssl: true, cert: certPath }),
};
The ...(condition && obj) admits conditional inclusion of properties.
Conditional value
const status = isActive ? "active" : "inactive";
const className = `btn ${isPrimary ? "btn-primary" : "btn-secondary"}`;
Discriminated union dispatch
type Action =
| { type: "INCREMENT" }
| { type: "DECREMENT" }
| { type: "SET"; value: number };
function reducer(state: number, action: Action): number {
if (action.type === "INCREMENT") return state + 1;
if (action.type === "DECREMENT") return state - 1;
if (action.type === "SET") return action.value;
return state;
}
For substantial discrimination, switch is conventionally clearer (treated in Pattern dispatch).
Validation chain
function validate(user: User): string | null {
if (!user.name) return "name is required";
if (user.age < 0) return "age must be non-negative";
if (user.email && !isValidEmail(user.email)) return "invalid email";
return null;
}
const error = validate(user);
if (error !== null) {
console.error(error);
return;
}
Optional callback
function process(items: Item[], onProgress?: (n: number) => void) {
items.forEach((item, i) => {
doWork(item);
onProgress?.(i + 1); // call only if provided
});
}
Conditional object property
const user = {
name: "Alice",
age: 30,
...(email && { email }), // include email only if truthy
...(phone ? { phone } : {}), // alternate form
};
Combined conditions
if (user && user.isActive && user.hasPermission("delete")) {
deleteResource();
}
The && short-circuits; the user.isActive access is safe because && evaluates left-to-right and stops on the first falsy.
The cleaner alternative with optional chaining:
if (user?.isActive && user?.hasPermission?.("delete")) {
deleteResource();
}
Truthiness-based default
const name = user.name || "anonymous"; // any falsy → default
// Use ?? if 0 or "" should be admitted:
const port = config.port ?? 8080;
Comparison: if vs match (Rust/Haskell) vs switch
Languages with substantial pattern matching (Rust, Haskell, Scala) admit dispatch on shape, not just on equality. TypeScript’s conventional substitutes:
if/else if— for boolean conditions and simple discriminated unions.switch— for value dispatch (treated separately).- Type guards and
instanceof— for type-based dispatch. in— for object-shape dispatch.
Combined patterns produce substantial expressiveness:
if (typeof x === "string") {
/* string handling */
} else if (Array.isArray(x)) {
/* array handling */
} else if (x instanceof Date) {
/* date handling */
} else if (x !== null && typeof x === "object" && "kind" in x) {
/* object with kind property */
}
Treated in Narrowing.
A note on if-as-statement
JavaScript and TypeScript treat if as a statement — it does not produce a value:
// Cannot:
// const max = if (a > b) a; else b;
// Use the ternary instead:
const max = a > b ? a : b;
// Or an IIFE for substantial computation:
const max = (() => {
if (a > b) return a;
return b;
})();
The conventional discipline is the ternary for simple cases and explicit let/const plus if/else for substantial computation.
A note on the conventional discipline
The contemporary TypeScript conditional advice:
- Use
if/else if/elsefor boolean dispatch. - Use the ternary for simple value-returning conditions.
- Use
??over||for default values that admit falsy values. - Use
?.for optional access. - Use
??=,||=,&&=for set-if-unset patterns. - Use early returns for precondition validation.
- Use type narrowing — the compiler tracks types through conditions.
- Use
switchover chainedelse iffor value dispatch. - Avoid deeply nested ternaries —
if/else ifis clearer for substantial branching. - Use braces consistently — the conventional style.
The combination — if/else, the ternary, ??, ?., the logical-assignment operators, automatic narrowing through conditions — is the substance of TypeScript’s conditional surface. The discipline produces clear, type-safe conditional code.