fix[DeepPartial, DeepReadonly, Simplify]: preserve branded primitive types - #2074
fix[DeepPartial, DeepReadonly, Simplify]: preserve branded primitive types#2074Tnalxmsk wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
🟡 Changes recommended
The new Simplify regression test doesn’t cover the nested branded-primitive case described as a goal of the PR.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a type-level regression in es-toolkit/types where branded primitive types (e.g. string & { __brand: ... }) were being treated as objects by DeepPartial, DeepReadonly, and Simplify, causing the brand to be lost after transformation. It does this by using the shared Primitive type as a terminal condition so primitives (including branded primitives) pass through unchanged.
Changes:
- Update
DeepPartial,DeepReadonly, andSimplifyto stop recursion/flattening whenT extends Primitive. - Add regression tests for branded primitives (direct inputs; plus nested for
DeepPartial/DeepReadonly). - Update reference docs in English/Korean/Japanese/Simplified Chinese to document primitive pass-through behavior.
File summaries
| File | Description |
|---|---|
| src/types/Simplify.ts | Treat Primitive as a terminal case so branded primitives aren’t flattened via the object/mapped-type branch. |
| src/types/Simplify.spec.ts | Adds a regression test for branded primitives (currently only direct, not nested). |
| src/types/DeepReadonly.ts | Adds Primitive to the pass-through branch to avoid recursively mapping branded primitives as objects. |
| src/types/DeepReadonly.spec.ts | Adds regression tests for branded primitives (direct + nested in object properties). |
| src/types/DeepPartial.ts | Adds Primitive to the pass-through branch to avoid recursively mapping branded primitives as objects. |
| src/types/DeepPartial.spec.ts | Adds regression tests for branded primitives (direct + nested in object properties). |
| docs/types/reference/objects/Simplify.md | Documents that primitives (including branded primitives) pass through unchanged. |
| docs/types/reference/objects/DeepReadonly.md | Documents that primitives (including branded primitives) pass through unchanged. |
| docs/types/reference/objects/DeepPartial.md | Documents that primitives (including branded primitives) pass through unchanged. |
| docs/ko/types/reference/objects/Simplify.md | Same documentation update (Korean). |
| docs/ko/types/reference/objects/DeepReadonly.md | Same documentation update (Korean). |
| docs/ko/types/reference/objects/DeepPartial.md | Same documentation update (Korean). |
| docs/ja/types/reference/objects/Simplify.md | Same documentation update (Japanese). |
| docs/ja/types/reference/objects/DeepReadonly.md | Same documentation update (Japanese). |
| docs/ja/types/reference/objects/DeepPartial.md | Same documentation update (Japanese). |
| docs/zh_hans/types/reference/objects/Simplify.md | Same documentation update (Simplified Chinese). |
| docs/zh_hans/types/reference/objects/DeepReadonly.md | Same documentation update (Simplified Chinese). |
| docs/zh_hans/types/reference/objects/DeepPartial.md | Same documentation update (Simplified Chinese). |
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| it('preserves branded primitive types', () => { | ||
| type UserId = string & { readonly __brand: 'UserId' }; | ||
|
|
||
| expectTypeOf<Simplify<UserId>>().toEqualTypeOf<UserId>(); | ||
| }); |
Summary
This PR follows up on the brand-preservation regression item listed under “Worth doing regardless” in #1986.
DeepPartial,DeepReadonly, andSimplifycurrently apply object-mapping logic to branded primitive types. As a result, the transformed type is no longer equivalent to the original branded primitive.The shared
Primitivetype introduced in [#2044](#2044) provides a common terminal condition for these utilities. Primitive types, including branded primitive types, can therefore pass through without being recursively mapped as objects.Changes
Primitivetype as a terminal condition inDeepPartial,DeepReadonly, andSimplify.DeepPartialandDeepReadonly.any[]function constraints inDeepPartialandDeepReadonlywithnever[], preserving parameterized function types without lint warnings.Implementation notes
This is a type-only change and does not affect runtime behavior. Reusing the existing
Primitivetype avoids duplicating the primitive union across the three utilities.The
never[]function constraint still matches arbitrary function signatures in the conditional type while avoiding an explicitany.