November 5, 2019·6 min read·engineeringtypescriptnullabilitytypes

TypeScript 3.7 makes nullish handling and assertion contracts explicit

Optional chaining and nullish coalescing distinguish absence from truthiness. Assertion signatures extend control-flow checking across validation helpers.

A missing object and a present object containing zero require different decisions. JavaScript's truthiness operators can obscure that distinction when used for property traversal and defaults. TypeScript 3.7 introduces optional chaining and nullish coalescing so those decisions can be expressed through null and undefined directly.

The November 5 release announcement explains why optional chaining took time to arrive. Its request was issue 16 on TypeScript's tracker, filed more than five years earlier. To avoid conflicting with eventual ECMAScript syntax, the team helped advance the proposal to stage 3 before shipping it.

That history matters because this is runtime syntax, with consequences for evaluation order and generated code. A shorthand for traversing objects needs a stable account of what it skips. The October beta announcement had already exposed the semantics for testing before this final release.

I find the new operators useful where they state the data contract more accurately. An empty field or a numeric zero can be legitimate input to an interface. Preserving that value while handling actual absence gives application code a more direct relationship to the decision it intends to make.

Optional access checks the value beside the operator

Optional property access returns undefined when its base is null or undefined. If the base is present, ordinary property access proceeds. In a longer chain, the placement of optional operators determines which intermediate values are allowed to be absent.

The distinction is visible in a typed example. This account interface allows both the profile object and its display name to be absent.

interface Account {
    profile?: {
        displayName?: string;
    };
}

function displayName(account?: Account) {
    return account?.profile?.displayName ?? "Anonymous";
}

The first optional access handles a missing account. The second handles a missing profile. Nullish coalescing then supplies a fallback if the selected display name is nullish. An existing empty string remains an empty string because it does not satisfy that narrower condition.

If an application instead requires a nonempty name, it needs to express that policy separately. The operator does not decide that an empty string is invalid. This keeps the syntax's absence condition distinct from validation rules that depend on the meaning of a field.

The release announcement contrasts optional chaining with repeated logical-AND checks. Logical AND short-circuits on all falsy values, including zero and false. Optional chaining narrows that condition to null and undefined, so replacing an expression requires understanding which behavior the original code intended.

Element access and calls have the same absence rule

Optional element access allows a computed key after the optional operator. The proposal's semantics apply the nullish check before evaluating that part of the chain. An index expression does not need to execute if the base is absent.

Optional call similarly handles an optional callback. If the callback is nullish, the call is skipped. If a value is present, it still needs to be callable. The operator does not convert a wrongly typed present value into a no-op.

These forms let an interface express optional operations without repeating temporary variables merely to check them. Their value is most apparent when the callback or nested object is optional by contract. Suppressing an unexpected absence at every access would be a different application policy.

The optional-chaining proposal also describes a chain as a syntactic unit. Short-circuiting can skip the property accesses and calls directly following the optional operator when its base is nullish. It does not make unrelated operations elsewhere in the expression conditional.

The result is precise enough to reason about side effects. A computed key within the skipped chain differs from a separate function call used in surrounding arithmetic. The program's structure determines which work belongs to the conditional operation.

A default should name its missing-value condition

Nullish coalescing returns the left operand unless it is null or undefined. In the missing case, it evaluates and uses the right operand. This permits a default without losing other falsy values.

For a numeric preference, zero is the straightforward example. A setting of zero can mean disabled or muted, while a missing setting means use the application default. Logical OR merges those cases. Nullish coalescing preserves the distinction without requiring a special comparison for zero.

The fallback expression is conditional work. If it calls a function, that function executes only when the left value is nullish. This matters when obtaining the default has a cost or a side effect. The operator is not a pair of eagerly evaluated candidate values.

A present but invalid number still remains present. Nullish coalescing does not reject NaN or enforce an allowed range. The caller must choose whether those values are valid according to its own contract, keeping default selection separate from validation.

Arithmetic continues outside an optional chain

The release announcement emphasizes that an expression such as an optional property access divided by a calculation still performs the division. The separate calculation also runs. If the chain returns undefined, the surrounding arithmetic receives that result.

Under strictNullChecks, TypeScript can identify the possibly undefined operand. That diagnostic is useful because the syntax has handled one condition without solving every consequence of absence. The author still needs to choose a numeric default or a control-flow path that avoids the arithmetic.

An explicit default before the division expresses one policy. An early return expresses another. The appropriate choice depends on whether absence should mean zero, missing output or an error. The optional operator's contract deliberately does not assign those application meanings.

This is a practical limit on shortening existing code. A chain may replace repetitive traversal checks, but it cannot automatically replace a branch whose body contains a larger operation. Reading the boundary of that operation remains necessary.

Assertion signatures carry evidence across a function call

The release adds another way to make a contract available to control-flow analysis. An assertion function can declare that returning successfully establishes a condition. Previously, authors often had to place a check directly in the caller for the compiler to retain its narrowing effect.

A helper can now validate an unknown value and communicate the result. This example uses a runtime typeof check before allowing its caller to perform a string operation.

function assertString(value: unknown): asserts value is string {
    if (typeof value !== "string") {
        throw new TypeError("Expected a string");
    }
}

function uppercase(value: unknown) {
    assertString(value);
    return value.toUpperCase();
}

The implementation throws for values outside the claimed type. After the call returns, the checker treats value as a string. It can then check the string method and catch a misspelled property instead of requiring an unrestricted type or an assertion at every use.

The signature does not generate the validation body. Its promise must agree with the function's implementation. An incorrect assertion helper can give the checker false evidence, just as an incorrect declaration can misdescribe a library.

This differs from a Boolean type predicate. A predicate gives the caller a result to branch on. An assertion function makes successful continuation itself the evidence, since failure exits by throwing. The two contracts fit different ways to organize validation.

Conditions can also be asserted directly

The other assertion form declares asserts condition. It tells the checker that the supplied condition is truthy if the helper returns. A condition such as a typeof comparison can then narrow the value in the containing scope.

This allows a general assertion helper to participate in ordinary type analysis. The caller can state the actual condition, while the helper owns the error-reporting behavior. The relationship avoids rewriting every validation as an inline throw merely to satisfy the compiler.

I prefer that arrangement when validation is already part of the program's runtime contract. The function boundary should not force the checker to discard a fact that successful execution establishes. A signature makes that fact explicit while keeping implementation responsibility with the helper's author.

The release also improves recognition of functions returning never. Calls to those functions can affect the control-flow graph without needing an artificial return statement around the call. That follows the same concern with representing whether execution can continue after an operation.

Existing JavaScript can supply declarations

The announcement additionally permits declaration generation together with allowJs. TypeScript can use its understanding of JavaScript and JSDoc to produce declaration files. That gives a mixed or JavaScript project another route to exposing types without converting all its implementation syntax.

The distinction between emitted declarations and runtime code remains important. A declaration describes what consumers can rely on for checking. It does not replace the JavaScript implementation or validate data crossing an external boundary.

TypeScript 3.7 therefore provides several explicit contracts around continuation and absence. Optional chains choose where traversal can stop, nullish coalescing selects a missing-value fallback, and assertion signatures state what successful validation establishes. The caller can combine those mechanisms while preserving the application-specific meaning of empty, invalid and absent values.