July 26, 2024·4 min read·engineeringtypescriptiteratorstypescompiler

TypeScript 5.6 beta distinguishes iterator output from completion

Iterator results have different meanings before and after completion. TypeScript 5.6 beta tightens the built-in return type and adds helper declarations while also detecting suspicious truthy expressions.

An iterator's value can represent either a yielded element or its final return value. Reading it without checking done conflates those cases. The TypeScript 5.6 beta, announced July 26, makes that distinction more useful in built-in iterator checking. It also adds declarations for iterator helper methods, making the types of iterator results more visible in ordinary transformations.

The compatibility problem comes from the historical default return type of any. Combining an element type with any loses the element information, so an unchecked result can evade diagnostics. The new built-in return mechanism lets strict checking preserve the possibility of completion instead of allowing any to absorb it. That is a specific repair to the information available at the call site.

I appreciate TypeScript's ability to explain these protocols through editor feedback. Its value in web applications depends on retaining useful information across library operations. If a default silently turns an ordinary result into any, the surrounding type annotations offer less protection than they appear to provide. Distinguishing completion restores a fact the program already needs to handle.

A result is a union of different states

The beta describes IteratorResult as a union of a yielded result and a completed result. A yielded result has a false or absent done and a value of the yield type. The completed result has done: true and the return type. A generator can yield strings and return a number, so those types need not agree.

Checking the discriminator before using the value respects that model. A small illustration reads a set iterator under strict checking:

const result = new Set(["queued", "running"]).values().next();

if (!result.done) {
  console.log(result.value.toUpperCase());
}

This does not assume that the set contains a value merely because its element type is string. Completion remains possible in the protocol. The guard identifies the result as a yielded element before applying a string operation. The type relationship follows the state of that result, not a prediction that the iterator must have more data.

The new BuiltinIteratorReturn intrinsic becomes undefined when strictBuiltinIteratorReturn is enabled, including through strict. Otherwise it remains any for compatibility. Built-in iterator declarations use that intrinsic for their return parameter. This improves common iterators without changing the historical default on every user-authored iterator type at once.

Helper methods need more than the minimal iterator protocol

The beta also introduces a type called BuiltinIterator for iterators supporting the new helper methods. Its published declaration file includes map, filter, and bounded operations such as take. A mapped iterator retains an iterator result instead of immediately requiring an array. A caller can request values as it consumes the result.

The naming distinction matters because TypeScript already has a general Iterator interface. A value satisfying that minimal protocol only needs the members required for iteration. It does not automatically have the new runtime prototype methods. The declarations consequently need to distinguish general protocol conformance from the built-in objects that provide the helpers.

Iterator.from adapts an existing iterator or iterable into the helper-capable form. This provides an explicit connection between the minimal protocol and the richer object. The compiler declarations still depend on a runtime implementing the proposal or an appropriate polyfill. Installing a new TypeScript compiler describes the methods but does not install JavaScript implementations into an older engine.

The helper declarations include a predicate-aware filter overload. A qualifying predicate can narrow the element type of the resulting iterator, while an ordinary callback retains the original element type. This carries the same distinction already useful for array filtering into iterator pipelines, without requiring the pipeline to materialize an array first.

Obvious expressions can expose bugs without deeper type analysis

The beta's truthy and nullish checks address a separate source of silent mistakes. A regular-expression object used directly as an if condition is truthy even when the author intended to test a string. An arrow function accidentally written instead of a comparison is also truthy. TypeScript now reports expressions whose syntax makes the suspicious result apparent.

Nullish coalescing has a related precedence trap. In value < limit ?? fallback, the comparison becomes the left operand. Its boolean result is never nullish, so the fallback cannot be selected. The diagnostic identifies that unreachable right operand. It does not need a general theorem about the application's values to recognize the expression's structure.

Certain constants remain permitted, including true, false, zero, and one. The announcement explains that intentional constant conditions remain useful during development and in loops. This keeps the check targeted at suspicious syntax instead of banning every expression with a predictable result.

I regard both improvements as useful because their errors have concrete explanations. Iterator code must distinguish a yielded element from completion. A regular-expression object does not perform a match merely by appearing in a condition. The beta makes those facts harder to overlook while retaining compatibility mechanisms for established iterator declarations and intentional constant conditions.