Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

TS2589 on update() for a relation-heavy model since 3.7.0

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

Maintainer thường phản hồi trong vòng 1 ngày

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

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
20/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
postgresql, typescript
Lĩnh vực
developer-experience

Hướng nghiên cứu

The types are emitted into the generated client, and the payload points at UpdateInput = XOR<UncheckedUpdateInput, CheckedUpdateInput>, UpdateRelationInput/UpdateRelationFieldPayload and SelectSubset<...> in the shipped dist/index.d.mts (and .d.cts), so start by finding the generator that emits them, changed in 3.7.0. A small project does not fail, so reproduce by running tsc (high heap) over a large multi-project program with the policy plugin. Done means TS2589 clears on that program while nested relation-form call sites still compile.

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

Mô tả

Description

Upgrading from 3.6.4 to 3.9.6 makes tsc fail with TS2589: Type instantiation is excessively deep and possibly infinite on an ordinary update() call against a model with a moderate number of relations. The same call type-checks fine on 3.6.4.

update(id: string, data: OrderUpdateArgs['data']): Promise<Order> {
  return this.db.order.update({
    where: { id },
    data: { ...data, updatedBy: userId },
  });
}
order.repository.ts:12:12 - error TS2589: Type instantiation is excessively deep and possibly infinite.
Found 1 error.
Which versions
Version Result
3.6.4 clean
3.7.0 TS2589
3.9.6 TS2589

3.7.0 is where the generator began emitting Checked/Unchecked create and update inputs per model, and where UpdateInput became XOR<UncheckedUpdateInput, CheckedUpdateInput>. That timing lines up with the error appearing, though I have not proven the XOR itself is the cause.

The call site cannot work around it

The error is reported on the update() call, and it survives every form I tried:

  1. Narrowing the parameter to OrderUncheckedUpdateInput.
  2. Building the payload as a separate annotated local.
  3. data: {...} as OrderUpdateArgs['data'].
  4. Asserting the whole argument: update({ where, data } as OrderUpdateArgs).

Even with the entire argument asserted, the error stands, which points at the signature rather than the argument expression:

update<T extends CrudArgsType<Schema, Model, 'update', Options, ExtQueryArgs, ExtResult>>(
  args: SelectSubset<T, CrudArgsType<Schema, Model, 'update', Options, ExtQueryArgs, ExtResult>, Options>
): ZenStackPromise<CrudReturnType<Schema, Model, 'update', T, Options, ExtResult>>;

SelectSubset<T, CrudArgsType<...>> has to be evaluated whatever the caller passes, so there is no call-site form that avoids it.

Correction: the schema is not the trigger

I originally implied schema size was the cause. Having tried to build a minimum repro, that is wrong, and I would rather say so than send you looking in the wrong place.

Taking our real schema — ~140 models, the failing model with ~35 fields and 2 relation fields, policies on most models — into a standalone project with nothing but @zenstackhq/orm, @zenstackhq/plugin-policy, pg and typescript, generating the client, and compiling the same update() call: it passes, with no errors at all.

I also could not reproduce it with a generated schema at or beyond our scale. All of these compiled clean:

Variable Tried up to
Models 142
Scalar fields per model 35
Relation fields per model 12
Enums (with @map-ed values) 240
Policies traversing relations and comparing to auth() 61 rules
Relation graph shape uniform, and hub-and-spoke with ~140 back-relations on a hub
Files each performing an update() and a create() 60

Same client typing as the app (InstanceType<typeof ZenStackClient<typeof schema>>), same experimentalDecorators + emitDecoratorMetadata, same skipLibCheck — none of it reproduces in isolation.

The error only appears inside our application's own compilation, which is large: one tsc program covering the API app and the libraries it imports through path aliases. Consistent with that, the reported line moves around as unrelated code in the file changes (:273, :275, :279 across edits), which is what you would expect if the compiler is hitting a budget rather than failing on one genuinely pathological expression.

So the honest statement of the problem is narrower than my original one: the update-input types are expensive enough that a sufficiently large program hits TypeScript's instantiation ceiling, and the same types are fine in a small one. The environment: PostgreSQL, policy plugin enabled, TypeScript 5.9, strict.

Cost

Even where it succeeds, checking this project needs roughly 4-5 GB: on Node's default heap tsc dies with JavaScript heap out of memory before it can report anything, and only reports the error when given --max-old-space-size=14336. A cold run takes 90-300s. On 3.6.4 the same project checks well inside the default heap.

Where the depth seems to come from

Reading the shipped .d.mts, the expansion looks like it compounds in three places:

  • UpdateInput is XOR<UncheckedUpdateInput, CheckedUpdateInput>, and XOR<T, U> builds (Without<T, U> & U) | (Without<U, T> & T). Without is itself a mapped type over the other side's keys, so each model yields two full key-by-key mapped types rather than one.
  • UpdateRelationInput maps over the model's relation fields to UpdateRelationFieldPayload, which expands to the create / createMany / connect / connectOrCreate / update / upsert set, each carrying a nested input for the related model — which repeats the whole construction, including its own XOR.
  • SelectSubset<T, U> is instantiated with U = CrudArgsType<..., 'update', ...>, and the method's constraint T extends CrudArgsType<...> references it too, so that type is evaluated to check the call regardless of what the caller passes.

The last point is why this cannot be worked around from user code: narrowing or asserting the argument only changes T, never U.

What would help

Options, roughly in order of how contained they look from the outside:

  1. Defer the nested relation payloads. UpdateRelationFieldPayload is only needed when a caller actually performs a nested write. If it expanded lazily, the common scalar-only update would not pay for the whole relation graph.
  2. Bound the recursion. A depth limit on nested relation inputs, degrading to a looser type past the limit, would keep deep or cyclic schemas finite. Cycles are normal once back-relations exist.
  3. Make the checked/unchecked choice per field rather than per model. The FK-vs-relation decision is really per relation field, but XOR applies it to the whole input and doubles the mapped types to do so. Discriminating per field, or via excess-property checking on a single mapped type, would avoid the doubling.
  4. Skip the XOR where it is vacuous. For a model with no relation fields the two halves coincide, so the union is pure overhead.
  5. Apply the same to create. CreateInput is XOR<UncheckedCreateInput, CheckedCreateInput> with the same nested-payload shape, so it should have the same profile even though update is what fails for us first.

Happy to test a patch against a real schema of this size, or to put together a minimal reproduction if that is more useful than these numbers.

Narrowing it down by experiment

I patched the shipped declarations (dist/index.d.cts and dist/index.d.mts) in place and re-ran a cold tsc against the real schema after each change. Both files have to be patched together: a CommonJS consumer resolves .d.cts, so patching only .d.mts silently changes nothing.

# UpdateInput shape Nested relation payloads TS2589
baseline XOR<Unchecked, Checked> present fires
A Omit<UncheckedUpdateInput, Without> (XOR dropped) partly present — UpdateNonOwnedRelationInput still in the unchecked half gone
B XOR<Unchecked, Checked> unchanged UpdateRelationInput and UpdateNonOwnedRelationInput replaced with {} gone

Neither ingredient triggers it on its own. In A the recursive relation payloads are still there in the unchecked half and the check passes; in B the XOR is still there and the check passes. It is the combination: XOR expands both operands, and each operand recursively expands relation payloads, so the recursive work happens twice over.

In both experiments the only remaining errors were the expected semantic fallout of the patch (call sites using the relation-object form against an input that no longer offers it), not TS2589.

A caveat on how much these experiments prove. Both of them remove capability rather than deferring it: with the payloads replaced by {}, the relation-object form (someRelation: { connect: … }) stops type-checking altogether, which is exactly why each run left errors at the call sites that use it. So they establish that the XOR and the recursive payloads are jointly necessary to reach the limit, and nothing more. They do not show that any particular fix is sufficient — deleting a type is trivially cheap, whereas deferring one may not be, since XOR still has to compare both operands' members.

On that basis I would treat a candidate fix as working only if both hold:

  1. TS2589 clears, and
  2. call sites using the nested relation form still compile.

Every experiment above fails (2) by construction. Option 2 (bounding the recursion) does look the least attractive of the listed options regardless, since it would cost inference on deeply nested writes.

Ngôn ngữ chính
TypeScript
Star
2.9k
Fork
157
Merge trung bình
11 giờ 42 phút
Pull request đã merge (30 ngày)
20

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

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.

Issue khác của zenstackhq/zenstack

Tất cả issue của zenstackhq/zenstack

Issue tương tự

Thêm issue về TypeScript

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.