TypeScript 3.6 models generator completion and array spread more accurately
Generator declarations preserve the difference between yielded items and terminal results. A new downlevel spread helper also corrects observable sparse-array behavior.
A generator's terminal result and its yielded items share a property name in the iterator protocol, but they do not necessarily share a type. TypeScript 3.6 makes the done flag carry enough information for the checker to distinguish them. The same release also corrects a different mismatch between a source operation and its representation, this time in emitted array-spread code.
The August 28 release announcement describes stricter generator checking alongside more accurate spread output for older targets. The changes belong to different parts of the toolchain. Generator checking improves what the compiler knows, while the spread correction changes what JavaScript it produces.
The release's iterable declarations show the new result model directly. IteratorResult is a union of a yield result and a return result. The done property distinguishes the two, preserving the relationship between the iteration state and its value.
I value this kind of precision in TypeScript because it helps the tooling explain the protocol a program actually uses. The benefit depends on preserving distinctions, including when two values occupy the same property but have different meanings at different stages of execution.
A generator's signature describes a conversation
The new Generator type separates the yielded type, terminal return type and input accepted through next. These are independent directions of data flow. A generator can produce a numeric progress value while accepting a Boolean instruction and eventually returning a string.
An explicit signature can document that exchange. The example below uses a Boolean to decide when numeric progress should stop and a terminal string should be returned.
function* progress(): Generator<number, string, boolean> {
let current = 0;
while (true) {
const stop = yield current++;
if (stop) {
return "complete";
}
}
}
const iterator = progress();
let step = iterator.next();
while (!step.done) {
console.log(step.value);
step = iterator.next(step.value >= 2);
}
console.log(step.value.toUpperCase());
The caller receives numeric values until it asks the generator to stop. The generator then returns the terminal string. Checking done before using value gives the checker evidence about which kind of result is present. The final string operation depends on completion, not on an assertion about an unknown union.
The Boolean supplied to next becomes the value of the suspended yield expression. It is not the numeric value that yield sent outward. Separating those types prevents a caller from accidentally treating the output type as the accepted input type.
The initial next call starts the generator without a value for an earlier suspension point. The declaration's rest parameter uses an empty-or-single-element tuple to allow zero or one argument. That call form coexists with checking the type of an argument when one is supplied.
Iterator results preserve a correlation
A yielded result has done false or absent and a value of the yielded type. A terminal result requires done true and supplies the return type. Modeling those as alternatives is stronger than giving value a union without relating it to the state.
The distinction lets ordinary control flow narrow a direct iterator result. In an earlier model, a completed generator could leave a caller with the union of yielded and returned types even after checking done. The runtime had supplied useful evidence that the type declaration failed to preserve.
Library authors can also use the Iterator interface when they need a more general iterator contract. Its return and throw methods are optional, whereas Generator guarantees those methods and is iterable. Choosing the narrower or broader interface is an API decision about what the consumer requires.
Defaults remain part of the model. The declaration does not eliminate every occurrence of any from iterator types. Supplying explicit return and input types is useful when an interface knows the protocol more precisely than those defaults express.
That is particularly relevant when a library exposes direct calls to next. A consumer using only a sequence of yielded items has different needs from a consumer that sends commands back into a generator. The expanded type parameters let the latter describe its additional contract without conflating it with the item type.
A sparse hole differs from an undefined value
Array spread presents a separate correctness problem. The release announcement uses an array created with a length but no populated slots. Spreading that array produces entries whose values are undefined. Copying it with slice preserves the holes instead.
A short JavaScript example shows an observable difference. The in operator asks whether the indexed property exists, distinguishing a hole from a present property with an undefined value.
const sparse = Array(3);
const spread = [...sparse];
const sliced = sparse.slice();
console.log(1 in sparse);
console.log(1 in spread);
console.log(1 in sliced);
The middle test is true because spread creates an entry at that index. The other two are false for the sparse arrays. All the arrays have the same length, so length alone does not reveal the semantic difference.
Before 3.6, the simpler downlevel transformation could use slice for this spread case. That output retained holes where the source operation created entries. A program examining whether an index exists could therefore observe different behavior after compilation to an older target.
The correction introduces the __spreadArrays helper for the relevant array-spread output. Its purpose is to model the source operation's slot behavior more accurately. This is a runtime-semantic correction in generated JavaScript, independently of whether a source file has a type error.
Downlevel iteration has a separate size tradeoff
The announcement explains that the more complete transformation for iteration constructs can be larger. TypeScript therefore has a simpler default for older targets that supports arrays, with downlevelIteration available for broader iteration behavior. The spread correction improves the simpler route's array behavior.
That context prevents an overbroad interpretation of the helper. It does not mean the simpler route suddenly supports every iterable under every target. The release addresses an observable array case while retaining a distinction between output strategies.
For application authors, the important consequence is that generated output must preserve more than the values a quick console display appears to show. Presence of an indexed property is part of JavaScript's observable behavior. An optimization that keeps length but changes that presence is not equivalent for all callers.
The release announcement notes that the helper is available through tslib. That provides an integration route for builds that use shared helpers. The mechanism still needs to be understood through the compiler's selected transformation and the runtime code the build supplies.
I find this correction relevant to the promise of using modern syntax with an older runtime target. The emitted program should preserve the source operation's behavior in the documented cases. A sparse-array edge case is still part of that contract when application or library code can observe it.
Promise diagnostics explain a common missing step
The release also improves diagnostics when code supplies a Promise where the resolved value is required. Its examples include passing a promised user record to a function expecting the record itself and calling a response method on a pending fetch result. The diagnostic can suggest that await is missing.
That suggestion does not insert an implicit wait into the application. The programmer still has to choose the intended asynchronous control flow. The improvement is explanatory, connecting a type mismatch with a common cause instead of leaving the author to infer it from unrelated property errors.
Quick fixes are available in some of those cases. Their usefulness depends on keeping the distinction between an automated edit and an application-level decision. Await changes where execution resumes, even if it resolves the immediate mismatch at the call site.
Build tools receive supported incremental APIs
The announcement also exposes APIs for incremental program construction and project-reference builds. It names createIncrementalProgram and createIncrementalCompilerHost for incremental work, plus createSolutionBuilder for project references. Third-party tools gain a supported way to access capabilities previously associated with compiler command modes.
ReadBuilderProgram can reconstruct prior builder information for use as an old program when creating another program. The returned object is not presented as a mutable program for arbitrary operations. This keeps persisted state and the active compiler program distinct in the public integration contract.
Those APIs matter because a project may use a build tool around TypeScript rather than invoking tsc alone. Exposing the relevant operations lets an integration participate without treating the compiler's private cache format as a stable interface. It also makes the intended reuse path more explicit. A build integration can retain the distinction between reusable information from an earlier compilation and the new program constructed to process the current source.
TypeScript 3.6 consequently improves accuracy in several places with different practical effects. Generator result declarations let control flow distinguish terminal returns from items, and next can describe the input traveling back into the generator. The array-spread helper separately preserves indexed-property presence in older-target output, where a superficially similar slice operation had produced a different program.