microsoft / microsoft/TypeScript

[5.9 regression] as const readonly tuple inferred as mutable through a nested generic call under a conditional-type circular constraint

Đang mở
#63,666 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Bug Domain: check: Type Inference
Ngôn ngữ chính
Go
Star
111k
Fork
14.3k
Merge trung bình
2 ngày 4 giờ
Pull request đã merge (30 ngày)
132

Mô tả

### 🔎 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

- **Worked in 5.8.3** (and in nightlies up to and including `5.9.0-dev.20250609`)
- **Broken since `5.9.0-dev.20250610`**, including 5.9.0-beta, 5.9.2, 5.9.3, and nightly `7.1.0-dev.20260720.1`
- Bisected via npm nightlies to the June 9, 2025 merge window. The commits in that window are #61828, #61837, #61668, #61805, #61822; the behavior change is consistent with **#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

```ts
interface Box {
readonly value: T
}
type Content = R extends Box ? U : never
type BoxLike = Box>

declare function box(value: T): Box

declare function f>(v: R): R

declare const a: { a: number }
declare const b: { b: number }

// ❌ 5.9: Box<[{ a: number }, { b: number }]> — readonly stripped from the as-const tuple
// ✅ 5.8.3: Box
const r1 = f(box([a, b] as const))

// ✅ { value: readonly [{ a: number }, { b: number }] } — same value as a direct
// object literal keeps readonly
const r2 = f({ value: [a, b] as const })

// ✅ Box<{ readonly n: 1; readonly s: "hi" }> — readonly *properties* and literal
// types survive; only readonly tuples/arrays are affected
const r3 = f(box({ n: 1, s: 'hi' } as const))

// ✅ Box — non-recursive constraint is fine
declare function g>(v: R): R
const r4 = g(box([a, b] as const))

// ✅ Box — circular constraint via indexed
// access (no conditional type) is also fine
declare function h>(v: R): R
const r5 = h(box([a, b] as const))
```

### 🙁 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`.

All three ingredients are required to trigger it:

1. the type parameter has a self-referential (F-bounded) constraint that goes through a **conditional type** — `R extends Box>` where `Content = R extends Box ? U : never`;
2. the argument flows through a **nested generic call** (`box(...)`) rather than being a direct literal — `r2` is fine — or a pre-typed variable;
3. 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`.

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>(body: () => R)` with an inline arrow or generator body), which is how this was originally hit: a Rust-style `Result` library exposing `gen>(body: () => Generator)` inferred `Result<[A, B], E>` from a body that returned `ok([a, b] as const)`.

### 🙂 Expected behavior

`r1` infers `Box`, 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`), #44821 (contextual inference at call sites with circular constraints).

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Tái hiện trường hợp `r1` trong Playground được liên kết bằng TypeScript 5.9.3 và so sánh kiểu được suy luận của nó với 5.8.3, sử dụng `r2`, `r4` và `r5` làm đối chứng. Đọc thay đổi hành vi liên quan đến #61668 và lần theo đường dẫn suy luận theo ngữ cảnh cho lời gọi `box` lồng nhau dưới ràng buộc vòng lặp có điều kiện; hoàn thành khi `r1` suy luận `Box` mà không làm cho `push` hợp lệ.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
typescript
Lĩnh vực
compilers
Loại issue
Lỗi
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.