Skip to content

fix[DeepPartial, DeepReadonly, Simplify]: preserve branded primitive types - #2074

Open
Tnalxmsk wants to merge 2 commits into
toss:mainfrom
Tnalxmsk:fix/preserve-branded-primitives
Open

fix[DeepPartial, DeepReadonly, Simplify]: preserve branded primitive types#2074
Tnalxmsk wants to merge 2 commits into
toss:mainfrom
Tnalxmsk:fix/preserve-branded-primitives

Conversation

@Tnalxmsk

@Tnalxmsk Tnalxmsk commented Aug 31, 2026

Copy link
Copy Markdown

Summary

This PR follows up on the brand-preservation regression item listed under “Worth doing regardless” in #1986.

DeepPartial, DeepReadonly, and Simplify currently apply object-mapping logic to branded primitive types. As a result, the transformed type is no longer equivalent to the original branded primitive.

type UserId = string & { readonly __brand: 'UserId' };

type PartialUserId = DeepPartial<UserId>;
type ReadonlyUserId = DeepReadonly<UserId>;
type SimplifiedUserId = Simplify<UserId>;

// Expected: all three types preserve UserId.

The shared Primitive type 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

  • Use the existing Primitive type as a terminal condition in DeepPartial, DeepReadonly, and Simplify.
  • Add direct branded-primitive regression tests for all three utilities, with additional nested coverage for DeepPartial and DeepReadonly.
  • Replace the explicit any[] function constraints in DeepPartial and DeepReadonly with never[], preserving parameterized function types without lint warnings.
  • Update the related reference documentation in English, Korean, Japanese, and Simplified Chinese.

Implementation notes

This is a type-only change and does not affect runtime behavior. Reusing the existing Primitive type 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 explicit any.

@Tnalxmsk
Tnalxmsk requested a review from raon0211 as a code owner August 31, 2026 07:09
Copilot AI lite review requested due to automatic review settings August 31, 2026 07:09
@Tnalxmsk
Tnalxmsk requested a review from dayongkr as a code owner August 31, 2026 07:09
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-toolkit Ready Ready Preview Aug 31, 2026 7:20am

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, and Simplify to stop recursion/flattening when T 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.

Comment on lines +5 to +9
it('preserves branded primitive types', () => {
type UserId = string & { readonly __brand: 'UserId' };

expectTypeOf<Simplify<UserId>>().toEqualTypeOf<UserId>();
});
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.

2 participants