Polyglot
Languages TypeScript conditionals
TypeScript § conditionals

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/falsyfalse, 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:

  • false
  • 0, -0, 0n (BigInt zero)
  • "" (empty string)
  • null
  • undefined
  • NaN

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/else for 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 switch over chained else if for value dispatch.
  • Avoid deeply nested ternariesif/else if is 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.