TypeScript 6.0 `--strictBuiltinIteratorReturn`: What Changes When Iterators Finally Have Correct Return Types
The iterator return type loophole that existed since TypeScript 2.3 is finally closing. Learn how --strictBuiltinIteratorReturn affects code that relies on iterator protocol semantics and what breaks when you flip the switch.
Most iterator type safety problems stem from a single design decision made in TypeScript 2.3: the return value of iterator.next() was typed as any for built-in iterators. This worked well enough for basic iteration, but it allowed dangerous type mismatches to slip through whenever code called .return() or .throw() on an iterator. TypeScript 6.0 closes this loophole with --strictBuiltinIteratorReturn, and the changes will surface bugs in code that assumed iterator protocol methods could accept arbitrary values.
The problem manifests when developers pass values to iterator methods that the iterator cannot actually handle. Array iterators, for example, ignore the argument to .return() entirely, but TypeScript allowed any type to flow through unchecked. This silent mismatch meant runtime behavior and type signatures diverged in ways that only became visible during refactoring or when debugging unexpected iterator state.
flowchart LR
Start("Iterator call site") --> Problem("Type system accepts any return value")
Problem --> Silent("Runtime ignores the value silently")
Silent --> Bug("Type signature lies about iterator protocol")
TypeScript 6.0 changes this by setting BuiltinIteratorReturn to undefined when --strictBuiltinIteratorReturn is enabled. This means built-in iterators now correctly reflect their actual behavior: they do not consume or propagate return values. The type system finally matches the JavaScript specification.
flowchart LR
Start("Iterator call site") --> Solution("Type system enforces undefined")
Solution --> Match("Runtime behavior matches types")
Match --> Safe("Iterator protocol is now type-safe")
style Safe stroke:#34d399,fill:#0b3b2e,color:#d1fae5
Key Takeaways
- Built-in iterators in TypeScript now return
undefinedinstead ofanywhen calling.return()or.throw()under--strictBuiltinIteratorReturn. - Code that passes values to iterator protocol methods will produce type errors, revealing previously silent bugs where those values were ignored at runtime.
- The flag is not part of
--strictby default in TypeScript 6.0, giving teams time to migrate before a future major version enforces it. - Migration requires auditing iterator-consuming code for manual
.next()calls with arguments, explicit.return()invocations, and generator functions that yield or return typed values. - Opt-out remains available for codebases that depend on legacy iterator libraries expecting the old
anybehavior.
Understanding BuiltinIteratorReturn and Why It Defaulted to any
The BuiltinIteratorReturn type parameter controls what TypeScript infers as the return type when you call .return(value) on a built-in iterator. Before TypeScript 6.0, this parameter defaulted to any, which meant the compiler accepted any value passed to iterator protocol methods without validation.
This design choice solved a practical problem in 2017: JavaScript iterators existed in the wild with inconsistent return value handling, and TypeScript needed to support them all without breaking existing code. The decision to use any made migration easier but introduced a type hole that developers exploited unknowingly.
When you enable --strictBuiltinIteratorReturn, TypeScript changes BuiltinIteratorReturn from any to undefined. This reflects the actual behavior of built-in JavaScript iterators: array iterators, string iterators, and set iterators all ignore the argument passed to .return(). The specification defines this as a no-op for these iterator types.
The consequence is immediate: code that passed typed values to .return() will now produce type errors. These errors are not false positives. They reveal places where your code assumed the iterator would consume a value, but the runtime ignored it. The type system was lying, and now it tells the truth.
flowchart TD
Call("iterator.return(value)") --> Check{"strictBuiltinIteratorReturn?"}
Check -->|disabled| Any("Accepts any value type")
Check -->|enabled| Undef("Enforces undefined only")
Any --> Runtime("Runtime ignores value")
Undef --> Runtime
Runtime --> Silent("No error at runtime")
style Silent stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
The type parameter matters most for generator functions and custom iterators that return meaningful values from .return(). A generator that yields numbers but returns a string when closed has a return type that must match the iterator protocol. Under the old rules, TypeScript allowed mismatches. Under the new rules, the types must align exactly.
How --strictBuiltinIteratorReturn Changes Iterator Type Inference
The flag affects how TypeScript infers the TReturn parameter in Iterator<T, TReturn, TNext>. Built-in iterators like arrays, maps, and strings now infer TReturn as undefined instead of any, which cascades through every function that accepts or returns an iterator.
This change is visible in three places: explicit iterator method calls, for-of loops that interact with iterator state, and generator functions that yield values. The most common breaks occur in code that manually drives an iterator by calling .next(), .return(), or .throw().
function consumeIterator<T>(iter: Iterator<T>): T[] {
const results: T[] = [];
let result = iter.next();
while (!result.done) {
results.push(result.value);
result = iter.next();
}
// This line now produces a type error
iter.return({ cleanup: true });
return results;
}
const numbers = [1, 2, 3][Symbol.iterator]();
consumeIterator(numbers);The error message is specific: Argument of type '{ cleanup: boolean }' is not assignable to parameter of type 'undefined'. The iterator does not accept a cleanup object because built-in iterators do not process return values. The code was always wrong, but TypeScript 5.x accepted it.
The fix depends on intent. If the goal was to signal cleanup to a custom iterator, the function signature must specify that the iterator accepts return values. If the goal was to close the iterator without passing data, remove the argument entirely.
// Correct approach 1: Accept only iterators with return values
function consumeIteratorWithCleanup<T>(
iter: Iterator<T, { cleanup: boolean }>
): T[] {
const results: T[] = [];
let result = iter.next();
while (!result.done) {
results.push(result.value);
result = iter.next();
}
iter.return({ cleanup: true });
return results;
}
// Correct approach 2: Close without passing data
function consumeIterator<T>(iter: Iterator<T>): T[] {
const results: T[] = [];
let result = iter.next();
while (!result.done) {
results.push(result.value);
result = iter.next();
}
iter.return();
return results;
}The second approach works with built-in iterators because .return() is optional and can be called without arguments. The type system now enforces this distinction.
Real-World Code That Breaks When You Enable This Flag
The most common break appears in middleware that wraps iterators for logging, rate-limiting, or transformation. These wrappers often pass arbitrary metadata to .return() under the assumption that the underlying iterator might use it. Under --strictBuiltinIteratorReturn, TypeScript rejects this unless the wrapper explicitly types the iterator to accept return values.
class RateLimitedIterator<T> implements Iterator<T> {
constructor(
private source: Iterator<T>,
private delay: number
) {}
next(): IteratorResult<T> {
return this.source.next();
}
return(value?: unknown): IteratorResult<T> {
// Type error: source.return expects undefined
return this.source.return({
rateLimited: true,
delay: this.delay
});
}
}The wrapper assumes the source iterator will handle the metadata, but built-in iterators ignore it. The fix is to remove the argument or change the source type to a custom iterator that accepts return values.
Another common break occurs in generator functions that return typed values. Before TypeScript 6.0, a generator could return any type without matching the iterator return signature. Now the types must align exactly.
function* generateWithMetadata(): Generator<number, string, void> {
yield 1;
yield 2;
// Return value must be string, not number
return 42; // Type error
}This catches real bugs. If code expected the generator to return a string for cleanup or logging, but the generator returned a number, the mismatch would cause silent failures downstream. TypeScript now surfaces this at compile time.
Comparing Iterator<T, any> vs Iterator<T, undefined> in Practice
The difference between these two signatures is subtle but expensive when it matters. An iterator typed with any as the return parameter accepts any value passed to .return(), while an iterator typed with undefined rejects everything except undefined.
flowchart LR
subgraph Old["Iterator<T, any>"]
OldCall("iter.return(value)") --> OldAccept("Accepts any type")
OldAccept --> OldRuntime("Runtime may ignore value")
end
subgraph New["Iterator<T, undefined>"]
NewCall("iter.return(value)") --> NewReject("Rejects non-undefined")
NewReject --> NewError("Compile-time error")
end
OldRuntime --> Mismatch("Type signature lies")
NewError --> Truth("Type signature matches runtime")
style Mismatch stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
style Truth stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The practical consequence appears in code that chains iterators or composes them with utility functions. A function that accepts Iterator<T, any> allows callers to pass built-in iterators with false type safety. A function that accepts Iterator<T, undefined> enforces correct usage at the call site.
Consider a function that exhausts an iterator and returns collected values. Under the old rules, the function could accept any iterator and call .return(metadata) without type errors, even though built-in iterators ignore metadata.
// Old signature (dangerous)
function exhaust<T>(iter: Iterator<T, any>): T[] {
const values: T[] = [];
let result = iter.next();
while (!result.done) {
values.push(result.value);
result = iter.next();
}
iter.return({ exhausted: true }); // Silently ignored
return values;
}
// New signature (safe)
function exhaust<T>(iter: Iterator<T>): T[] {
const values: T[] = [];
let result = iter.next();
while (!result.done) {
values.push(result.value);
result = iter.next();
}
iter.return(); // Type-safe
return values;
}The new signature prevents callers from passing values to .return(), which matches runtime behavior. The old signature allowed it, which created a false impression that the iterator would process the value.
This distinction is critical when migrating large codebases. Functions typed with Iterator<T, any> must be audited manually because TypeScript cannot infer which iterators actually use return values and which ignore them. The compiler can only check syntax, not intent.
Migration Strategy: Fixing Iterator-Based Code for TypeScript 6.0
The migration path starts with enabling --strictBuiltinIteratorReturn in a test environment and collecting type errors. These errors fall into three categories: explicit .return() calls with arguments, generator functions with mismatched return types, and utility functions that accept overly permissive iterator signatures.
flowchart LR
Enable("Enable strictBuiltinIteratorReturn") --> Compile("Run tsc --noEmit")
Compile --> Errors("Collect type errors")
Errors --> Audit("Audit each error for intent")
Audit --> Fix{"Can remove argument?"}
Fix -->|yes| Remove("Remove return argument")
Fix -->|no| Retype("Change iterator type")
Remove --> Test("Run tests")
Retype --> Test
Test --> Deploy("Deploy when green")
style Deploy stroke:#34d399,fill:#0b3b2e,color:#d1fae5
For each error, the first question is whether the return value was intentional. If the code called .return(metadata) but the iterator is a built-in array or string iterator, the argument was always ignored. Remove it. If the code called .return(value) on a custom iterator that actually processes the value, change the type signature to reflect that.
Generator functions require special attention because their return type must match both the explicit return statement and the iterator protocol. A generator that yields numbers and returns a string must be typed as Generator<number, string, void>, not Generator<number>.
// Before: implicit any return
function* processItems(items: string[]) {
for (const item of items) {
yield item.length;
}
return "complete"; // Implicitly any
}
// After: explicit return type
function* processItems(items: string[]): Generator<number, string, void> {
for (const item of items) {
yield item.length;
}
return "complete";
}The third category involves utility functions that wrap or transform iterators. These functions must decide whether they accept only built-in iterators or support custom iterators with return values. The choice affects the type signature and determines which iterators can be passed.
A transformer that only works with built-in iterators should use Iterator<T> or Iterator<T, undefined>. A transformer that supports custom iterators should use Iterator<T, TReturn> and propagate the return type through the transformation.
// Built-in iterators only
function map<T, U>(
iter: Iterator<T>,
fn: (value: T) => U
): Iterator<U> {
return {
next() {
const result = iter.next();
if (result.done) {
return result;
}
return { done: false, value: fn(result.value) };
},
return() {
iter.return?.();
return { done: true, value: undefined };
}
};
}
// Custom iterators with return values
function map<T, U, TReturn>(
iter: Iterator<T, TReturn>,
fn: (value: T) => U
): Iterator<U, TReturn> {
return {
next() {
const result = iter.next();
if (result.done) {
return result;
}
return { done: false, value: fn(result.value) };
},
return(value?: TReturn) {
return iter.return?.(value) ?? { done: true, value: undefined as any };
}
};
}The second version preserves the return type, which allows custom iterators to propagate cleanup signals or final values through the transformation. The first version discards return values, which is correct for built-in iterators but breaks custom iterator semantics.
When to Explicitly Opt Out of strictBuiltinIteratorReturn
Opt-out is necessary when migrating a codebase that depends on third-party iterator libraries written before TypeScript 6.0. These libraries often typed iterator return values as any because that was the only way to support both built-in and custom iterators without excessive type annotations.
The decision to opt out should be temporary. Long-term type safety depends on migrating these libraries or wrapping them with correct types. Disabling the flag indefinitely means accepting silent bugs in iterator protocol code.
flowchart LR
Evaluate("Evaluate dependency") --> Check{"Library updated?"}
Check -->|yes| Upgrade("Upgrade library version")
Check -->|no| Fork{"Can fork/patch?"}
Fork -->|yes| Patch("Apply type patches")
Fork -->|no| OptOut("Disable flag temporarily")
Upgrade --> Enable("Enable strictBuiltinIteratorReturn")
Patch --> Enable
OptOut --> Schedule("Schedule migration sprint")
style OptOut stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
A second reason to opt out is when the codebase uses iterator-heavy abstractions that would require weeks of refactoring to align with the new types. In this case, the team should plan a dedicated migration sprint rather than attempting to fix everything during a routine upgrade.
The opt-out configuration is straightforward. Set strictBuiltinIteratorReturn: false in tsconfig.json to disable the flag even when --strict is enabled. This allows incremental migration without blocking other strict mode features.
{
"compilerOptions": {
"strict": true,
"strictBuiltinIteratorReturn": false
}
}Teams should track the opt-out as technical debt and prioritize migration before TypeScript 7.0, where the flag will likely become non-optional under --strict. The type system is moving toward correctness, and delaying migration only increases the eventual cost.
Frequently Asked Questions
Does enabling strictBuiltinIteratorReturn break for-of loops?
No. For-of loops do not call .return() explicitly, so they remain type-safe regardless of the flag. The flag only affects code that manually drives iterators by calling protocol methods.
Can I enable this flag without enabling all of strict mode?
Yes. The flag is independent and can be enabled even when strict is false. Set "strictBuiltinIteratorReturn": true in tsconfig.json to opt in selectively.
Why did TypeScript wait until 6.0 to make this change?
The change required breaking existing code that relied on the any behavior, which violates TypeScript's stability guarantees for minor versions. Major versions allow breaking changes, so TypeScript 6.0 was the first opportunity to fix this long-standing type hole.
What happens to generators that return void?
Generators that return void are unaffected. The flag only changes inference for iterators that explicitly return typed values. A generator with no return statement or a return statement without a value continues to work as before.
Do I need to update third-party iterator libraries?
Only if those libraries expose iterator return types in their public API. Libraries that return opaque iterators or use Iterator<T> without specifying return types will continue to work without changes.
Conclusion: Why This Flag Matters for Long-Term Type Safety
The --strictBuiltinIteratorReturn flag closes a type safety gap that existed since TypeScript 2.3. Built-in iterators now have correct return types that match their runtime behavior, which prevents silent bugs where code passed values to iterator methods that ignored them. The migration cost is real, but the payoff is a type system that tells the truth about iterator protocol semantics.
Teams should enable the flag during the next major upgrade cycle and plan time to fix type errors systematically. The errors reveal places where types and runtime behavior diverged, which means fixing them improves code quality beyond satisfying the compiler. That covers the essential patterns for TypeScript 6.0 iterator types. Apply these in production and the difference will be immediate.
For broader context on migrating to TypeScript 6.0, see TypeScript 5 to 6 Migration: 200K Lines and TypeScript 6 Migration Guide. If you work with form validation where iterator types surface in field processing, TypeScript Form Validators Custom covers the practical implications.