Skip to content

fix: avoid excessive Draft recursion for readonly arrays in recursive types - #1311

Open
huytdps13400 wants to merge 1 commit into
immerjs:mainfrom
huytdps13400:fix/draft-readonly-array-recursion
Open

huytdps13400 wants to merge 1 commit into
immerjs:mainfrom
huytdps13400:fix/draft-readonly-array-recursion

Conversation

@huytdps13400

Copy link
Copy Markdown

Fixes #839

Root cause

#1197 stopped the Draft<T> recursion for recursive types by mapping plain arrays to Draft<T[number]>[], but WritableDraft only takes that branch for T extends any[]. Readonly arrays still go through the mapped type, so recursive types that use readonly arrays still fail with TS2589: Type instantiation is excessively deep and possibly infinite. One example is type-fest's current JsonValue, where JsonArray = JsonValue[] | readonly JsonValue[]:

type JsonValue = string | number | boolean | null | JsonObject | JsonArray
type JsonObject = {[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined}
type JsonArray = JsonValue[] | readonly JsonValue[]

produce({} as JsonObject, draft => { draft.a = 1 }) // TS2589 on main

Change

WritableDraft<T> now checks T extends readonly any[], so readonly plain arrays take the same Draft<T[number]>[] branch as mutable ones. The resulting draft type is the same as before (a mutable array of drafted elements). Readonly tuples, including variadic ones, still go through the number extends T["length"] / IsPlainArray checks, so they keep their tuple shape.

Verification

  • Added a case to __tests__/type-external.ts that uses the type-fest-shaped JsonValue with readonly arrays.
  • tsc --noEmit -p __tests__:
    • before: type-external.ts(29,12): error TS2589 and (30,3): error TS2589
    • after: neither error. The only remaining errors are 5 existing TS2802 errors in src/plugins/arrayMethods.ts, which also show up on main with this tsconfig.
  • I also checked by hand that readonly tuples, variadic readonly tuples, readonly T[] & {...} intersections and generic readonly A[] states produce the same types as before. With a generic A, castDraft(a) can now be pushed into a drafted readonly A[], which failed before.
  • yarn test:src and yarn test:build pass. yarn test:flow doesn't run on my machine (the flow binary won't spawn on arm64), and it fails the same way on main.

This fix was prepared with AI assistance (Claude Code) and verified locally with the tests above.

🤖 Generated with Claude Code

… types

WritableDraft only took the `Draft<T[number]>[]` shortcut for mutable
arrays, so readonly arrays went through the mapped type and recursive
types such as type-fest's JsonValue (which uses `readonly JsonValue[]`)
still hit TS2589. Match `readonly any[]` so readonly plain arrays take
the same path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript - Recursive types: Type instantiation is excessively deep and possibly infinite - ts(2589)

1 participant