Skip to content

Comments

UnionMember: export internal type, useful to recurse union-types. #1368

Open
taiyakihitotsu wants to merge 10 commits intosindresorhus:mainfrom
taiyakihitotsu:add/last-of-union-20260204
Open

UnionMember: export internal type, useful to recurse union-types. #1368
taiyakihitotsu wants to merge 10 commits intosindresorhus:mainfrom
taiyakihitotsu:add/last-of-union-20260204

Conversation

@taiyakihitotsu
Copy link
Contributor

@taiyakihitotsu taiyakihitotsu commented Feb 14, 2026

Add UnionMember

  • This is useful to recurse union-types, so I think this should be exported as a global for end-users.
  • We can evade a duplicated definition potentially.

And the current UnionMember, which locally defined in UnionToTuple, returns unknown.

expectType<never>({} as UnionMember<never>);
//=> unknown

This would cause an error if end-users use IsNever for termination check.

Ideally, UnionMember reduces a type, e.g., assuming $A$ and $B$ are type arguments of UnionMember<A | B>, $A \cup B$ -> $B$, so $\emptyset$ -> $\emptyset$ intuitively ($\bot$ -> $\bot$).
This PR's UnionMember handles this.


Note

taiyakihitotsu and others added 3 commits February 15, 2026 09:26
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
@taiyakihitotsu
Copy link
Contributor Author

@sindresorhus

Thanks for your reviews!

And I believe this suggestion implies that test-d/last-of-union.ts should have a test for recursion without directly importing UnionToTuple to clarify it. Right?

If so, UnionToTuple in test-d/last-of-union.ts fulfills the requirement. Would it be okay to integrate both in test-d/last-of-union.ts?


#1368 (comment)

type UnionToTupleWithExclude<T, L = LastOfUnion<T>> =
	IsNever<T> extends false
		? [...UnionToTupleWithExclude<Exclude<T, L>>, L]
		: [];

expectType<1 | 2 | 3>({} as UnionToTupleWithExclude<1 | 2 | 3>[number]);

: never;

type DifferentModifierUnion = {readonly a: 0} | {a: 0};
expectType<DifferentModifierUnion>({} as UnionToTuple<DifferentModifierUnion>[number]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Umm...I don't think this approach for testing is ideal where we implement something with LastOfUnion and then test the implementation. We'll have to find a better way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's just use something similar to UnionToTuple, like:

type Test<T, L = LastOfUnion<T>> =
IsNever<T> extends false
	? Test<Exclude<T, L>> | L
	: never;

expectType<1 | 2 | 3>({} as Test<1 | 2 | 3>);

Also, remove everything else. Here, we are not trying to test quirks of a specific implementation, such as Exclude behaving unexpectedly in certain cases. The goal here is to test LastOfUnion only, so the surrounding implementation should be as minimal as possible.

Copy link
Collaborator

@som-sm som-sm Feb 15, 2026

Choose a reason for hiding this comment

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

type Test<T, L = LastOfUnion<T>> =
IsNever<T> extends false
	? Test<Exclude<T, L>> | L
	: never;

Even this is not ideal, because it does not guarantee that LastOfUnion picks exactly one member at a time. For example, in the 1 | 2 | 3 case, if LastOfUnion<1 | 2 | 3> returns 1 | 2 first, and then LastOfUnion<Exclude<1 | 2 | 3, 1 | 2>> returns 3, the test would still pass.

This is a better approach I guess:

type Test<T, L = LastOfUnion<T>> =
IsNever<T> extends false
	? Test<Exclude<T, L>> | [L]
	: never;

expectType<[1] | [2] | [3]>({} as Test<1 | 2 | 3>);

I hope this makes sense?

Copy link
Contributor Author

@taiyakihitotsu taiyakihitotsu Feb 15, 2026

Choose a reason for hiding this comment

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

I kept only this definition.
#1368 (comment)


After both are merged, #1368 and #1349, then open a PR to fix test-d/union-member.ts to add the readonlyExcludeExactly case.
Or include the test case into #1349 (and merge this after #1368).

Does this sound acceptable?
If so, which approach do you prefer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, our comments crossed!

Even this is not ideal, because it does not guarantee that LastOfUnion picks exactly one member at a time.

I’ve added your test case to test-d/union-member.ts and included a comment explaining the scenario it covers.

Copy link
Contributor Author

@taiyakihitotsu taiyakihitotsu Feb 15, 2026

Choose a reason for hiding this comment

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

After both are merged, #1368 and #1349, then open a PR to fix test-d/union-member.ts to add the readonlyExcludeExactly case.

And I've updated this my comment, because this is NOT readonly but ExcludeExacly case.

@taiyakihitotsu taiyakihitotsu changed the title LastOfUnion: export internal type, useful to recurse union-types. UnionMember: export internal type, useful to recurse union-types. Feb 15, 2026

This comment was marked as resolved.

Repository owner deleted a comment from Copilot AI Feb 17, 2026
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.

3 participants