June 21, 2022·4 min read·engineeringtypescriptgenericsnullability

TypeScript 4.8 beta simplifies the type left after a null check

TypeScript 4.8 beta represents NonNullable with an intersection. That gives generic null checks a result the checker can reduce and compare directly.

Checking a generic value for null should provide enough evidence to return it as non-nullable. Earlier TypeScript analysis could recognize the runtime check and still reject the declared return type. The 4.8 beta changes the type representation that connects those two operations, making the proof available without an assertion.

The June 21 announcement describes a group of related changes under strictNullChecks. Intersection reduction and union compatibility support new narrowing behavior. The important change for application code is that a generic value can acquire the same non-nullable form the standard utility type expresses.

I appreciate this kind of TypeScript improvement because it reduces the distance between a visible check and the compiler's conclusion. An assertion beside a correct runtime guard often communicates less than the guard itself. If the type representation can carry that evidence, the implementation can remain ordinary JavaScript control flow.

Empty braces do not mean objects only

The implementation pull request explains that the empty object type literal is a supertype of all types except null and undefined. Its spelling can be misleading if read as a restriction to object instances. Numbers and strings also belong to this non-nullish category.

That property makes an intersection with empty braces useful. For a generic type T, T & {} represents the part of T that excludes null and undefined. Intersections with those excluded types reduce to never, while supported non-nullish cases retain the corresponding type.

The beta rewrites NonNullable using that intersection. Previously, the utility used a conditional type. The announcement explains that intersections can be reduced and assigned to in ways that the conditional representation could not, particularly when the utility appears more than once.

For example, applying NonNullable to an already non-nullable generic type should not create an operationally different contract. The new representation lets repeated applications simplify and makes assignments between those forms work more consistently. That matters in generic libraries where one helper's output becomes another helper's input.

Runtime evidence matches the utility type

The beta can intersect a generic value with empty braces after a check excludes both null and undefined. Since that is also the new definition of NonNullable, the declared return type and the narrowed expression agree.

function requireValue<T>(value: T): NonNullable<T> {
    if (value === null || value === undefined) {
        throw new Error("Missing value")
    }
    return value
}

The guard performs the runtime validation. The utility type describes the successful result. No assertion asks the checker to trust information beyond the branch condition, and the function preserves every non-nullish input value unchanged.

A truthiness test has different runtime behavior. It also rejects values such as zero and the empty string. The beta's analysis can use a non-nullish intersection in a truthy branch, but that does not make a truthiness guard interchangeable with the explicit check above.

The distinction matters for reusable helpers. A function called to require presence should not silently reject a valid numeric zero merely because the type system can narrow a truthiness branch. The implementation's runtime predicate remains the source of the operation's actual contract.

The value returned by the helper still has its original identity. A type-level intersection does not copy an object or wrap a primitive during execution. That distinction keeps the change relevant to existing APIs. The compiler gains a more useful description of successful control flow without requiring the function to manufacture a new representation for the caller.

Unconstrained generics need a reason to exclude null

The same work closes an older compatibility allowance. Under strict null checking, an unconstrained type parameter can include null or undefined. Passing such a value into a position that excludes them is consequently unsafe.

The beta now reports that case instead of assuming the parameter is acceptable. A helper can express the non-nullish requirement as a generic constraint, or it can validate the value before passing it onward. Those choices establish different interfaces, depending on whether absence should be rejected by callers' types or handled during execution.

The implementation also narrows unknown more consistently with the union of empty braces, null and undefined. A truthy branch excludes the nullish cases, while the false branch still needs to represent values that a broad unknown type could contain. The checker cannot infer a complete runtime schema from one conditional.

I prefer fixing the declaration or adding the required runtime check to inserting an assertion just to preserve compilation. The new diagnostic points to an unproven premise about the generic value. The best repair is the one that states where the program actually establishes that premise.

The pull request notes compatibility exceptions for explicitly written intersections such as string & {}, used by some libraries to retain completion hints alongside literal alternatives. Reduction therefore has deliberately documented edge cases. The main utility remains straightforward for generic code. A real null check produces an intersection the checker can compare directly with the non-nullable result the function promises.