You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
interfaceBox<T>{readonlyvalue: T}typeContent<R>=RextendsBox<infer U> ? U : nevertypeBoxLike<R>=Box<Content<R>>declarefunctionbox<T>(value: T): Box<T>declarefunctionf<RextendsBoxLike<R>>(v: R): Rdeclareconsta: {a: number}declareconstb: {b: number}// ❌ 5.9: Box<[{ a: number }, { b: number }]> — readonly stripped from the as-const tuple// ✅ 5.8.3: Box<readonly [{ a: number }, { b: number }]>constr1=f(box([a,b]asconst))// ✅ { value: readonly [{ a: number }, { b: number }] } — same value as a direct// object literal keeps readonlyconstr2=f({value: [a,b]asconst})// ✅ Box<{ readonly n: 1; readonly s: "hi" }> — readonly *properties* and literal// types survive; only readonly tuples/arrays are affectedconstr3=f(box({n: 1,s: 'hi'}asconst))// ✅ Box<readonly [{ a: number }, { b: number }]> — non-recursive constraint is finedeclarefunctiong<RextendsBox<unknown>>(v: R): Rconstr4=g(box([a,b]asconst))// ✅ Box<readonly [{ a: number }, { b: number }]> — circular constraint via indexed// access (no conditional type) is also finedeclarefunctionh<RextendsBox<R['value']>>(v: R): Rconstr5=h(box([a,b]asconst))
🙁 Actual behavior
In r1, the as const tuple loses its readonly modifier: R is inferred as Box<[{ a: number }, { b: number }]> (mutable tuple). In 5.8.3 the same code inferred Box<readonly [{ a: number }, { b: number }]>.
All three ingredients are required to trigger it:
the type parameter has a self-referential (F-bounded) constraint that goes through a conditional type — R extends Box<Content<R>> where Content<R> = R extends Box<infer U> ? U : never;
the argument flows through a nested generic call (box(...)) rather than being a direct literal — r2 is fine — or a pre-typed variable;
the inferred type contains a readonly tuple. Readonly object properties and literal types under the same as const survive (r3).
Removing any one ingredient preserves readonly: a closed constraint (r4) or an indexed-access circular constraint with no conditional type (r5) both infer Box<readonly [{ a: number }, { b: number }]>.
Ingredients 1 and 2 line up with the change described in #61668: since that PR, the return type of the inner box(...) call is checked against a contextual type in which the outer R has been instantiated to its constraint — here a conditional-type-based self-referential constraint — and somewhere in that inference path the tuple candidate's readonly modifier is dropped.
The same trigger also affects context-sensitive callback arguments (f2<R extends BoxLike<R>>(body: () => R) with an inline arrow or generator body), which is how this was originally hit: a Rust-style Result library exposing gen<Y, R extends ResultLike<R>>(body: () => Generator<Y, R>) inferred Result<[A, B], E> from a body that returned ok([a, b] as const).
🙂 Expected behavior
r1 infers Box<readonly [{ a: number }, { b: number }]>, as it did in 5.8.3 and consistent with r2, r4, and r5, which differ only in details that should not affect the tuple's readonly-ness. A const assertion produces a non-widening readonly tuple; inference should not convert it to a mutable one.
Note the failure mode is silent unsoundness rather than an inference error: r1.value.push(...) type-checks against a frozen-in-intent tuple.
Additional information about the issue
Possibly related: #62071 (another inference regression linked to #61668), #51377 (inference failure with T extends F<T>), #44821 (contextual inference at call sites with circular constraints).
🔎 Search Terms
readonly tuple stripped, as const widening, mutable tuple inference, circular constraint, F-bounded constraint, conditional type constraint, contextual inference, 5.9 regression
🕗 Version & Regression Information
5.9.0-dev.20250609)5.9.0-dev.20250610, including 5.9.0-beta, 5.9.2, 5.9.3, and nightly7.1.0-dev.20260720.1import defer =parsing #61837, Fix type variable leaks and cache inconsistencies #61668, Add--module node20#61805, optimization, reduce memory usage #61822; the behavior change is consistent with Fix type variable leaks and cache inconsistencies #61668 ("Fix type variable leaks and cache inconsistencies"), which made outer type parameters in contextual types instantiate (possibly to their constraints) before inferences are made to return types of inner function calls — matching this repro's requirement of a nested generic call under a constraint-derived contextual type (see below).⏯ Playground Link
https://www.typescriptlang.org/play/?ts=5.9.3#code/JYOwLgpgTgZghgYwgAgEIHsAeAeAKgPmQG8AoZZKCOAE3RABsBPZANznoFcIAuZXEgL4kwjAA4oAwnUjhsAJUIBeZHOQRMM6gGc0WbKBjRkAVUIB+E8l4gILaMLEoMmADLAA1hHlLdOKeAhZBXwSEmoIBHo4SmQYDhAEMGA6ZAAjPQIACjZOHj4ASl5nPBCwiKiYuISklJh5NQ1A7V83T298bN45QpVQ8MjolAQ6LTBkOF4icesOAFtUoyF+iqGRsdTJtJn5xdCAej3kQBlyZABWADoATiK9AG0pieQQOYWoZAEAGmItp5fFgF1CIAUAgoVFoDGYoygwFE4mosSg6FmyDAAAsUHAtABaYYgUYojiiegQEgHZCAUHIzucABznADMNxwlBodCYyHu01+Ozen2+Gy5r3egJIuPxUAAjMhlDBMulMJlbnAvql-uMdKKwPl8vtDpSpjkuLxmeC2RzHs9ue8vlN+RbBQJVQJkCCtHBZigDRidHBkNRgJREqTDuhUgArCJjejASBQdjITwQUQ6Y2sxgitYUABMUtimX17EN7KVaVVmOQGve2qDFN82CmKYhT144oA3KCWY2tLwAESo4Dd97A9sm5gAKlEiPEUCSEC0o-GIHhUZj7GrInEOi0HCgLGAdjbqeHh7AhOJWj20VjjG9MTgMEMiQg1HTeLGUDpOZlcrzTeQ4q+XbIAA5H2QHvGq5ZrFqOo1sUDamg82z2taPx2gCQ4gHQWIBtuWh7qsr6xqAYzADoMCgCSyyDLE8SJMkIDIAA5vU6iaDoxTxO4mEAO4gPgHQsF0PRyC+YoACw5oxspYAqxYqhBGrQdWlJwWCh5mkhiwobafw8oCzrlv6CAcBUkGEXAxGsMAPqgOEmBPtWiBIFoOiZJhZl+jUIBxuuED5MgpHjPQWjoLEFFlAMlS0V5yCoixjSLuxehyLcQGekBgICUJXSiW+pw5qi0nyoqyqluqUHakAA
💻 Code
🙁 Actual behavior
In
r1, theas consttuple loses itsreadonlymodifier:Ris inferred asBox<[{ a: number }, { b: number }]>(mutable tuple). In 5.8.3 the same code inferredBox<readonly [{ a: number }, { b: number }]>.All three ingredients are required to trigger it:
R extends Box<Content<R>>whereContent<R> = R extends Box<infer U> ? U : never;box(...)) rather than being a direct literal —r2is fine — or a pre-typed variable;as constsurvive (r3).Removing any one ingredient preserves
readonly: a closed constraint (r4) or an indexed-access circular constraint with no conditional type (r5) both inferBox<readonly [{ a: number }, { b: number }]>.Ingredients 1 and 2 line up with the change described in #61668: since that PR, the return type of the inner
box(...)call is checked against a contextual type in which the outerRhas been instantiated to its constraint — here a conditional-type-based self-referential constraint — and somewhere in that inference path the tuple candidate'sreadonlymodifier is dropped.The same trigger also affects context-sensitive callback arguments (
f2<R extends BoxLike<R>>(body: () => R)with an inline arrow or generator body), which is how this was originally hit: a Rust-styleResultlibrary exposinggen<Y, R extends ResultLike<R>>(body: () => Generator<Y, R>)inferredResult<[A, B], E>from a body that returnedok([a, b] as const).🙂 Expected behavior
r1infersBox<readonly [{ a: number }, { b: number }]>, as it did in 5.8.3 and consistent withr2,r4, andr5, which differ only in details that should not affect the tuple's readonly-ness. Aconstassertion produces a non-widening readonly tuple; inference should not convert it to a mutable one.Note the failure mode is silent unsoundness rather than an inference error:
r1.value.push(...)type-checks against a frozen-in-intent tuple.Additional information about the issue
Possibly related: #62071 (another inference regression linked to #61668), #51377 (inference failure with
T extends F<T>), #44821 (contextual inference at call sites with circular constraints).