Skip to content

Commit 91929af

Browse files
committed
feat: add ExclusifyUnion type
1 parent 5612f64 commit 91929af

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

source/exclusify-union.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type {NonRecursiveType} from './internal/type.d.ts';
2+
import type {KeysOfUnion} from './keys-of-union.d.ts';
3+
import type {Simplify} from './simplify.d.ts';
4+
import type {UnknownArray} from './unknown-array.d.ts';
5+
6+
export type ExclusifyUnion<Union> =
7+
Extract<Union, NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | UnknownArray> extends infer SkippedMembers
8+
? SkippedMembers | _ExclusifyUnion<Exclude<Union, SkippedMembers>>
9+
: never; // Should never happen
10+
11+
type _ExclusifyUnion<Union, UnionCopy = Union> = Union extends unknown // For distributing `Union`
12+
? Simplify<
13+
Union & Partial<Record<Exclude<KeysOfUnion<UnionCopy>, keyof Union>, never>>
14+
>
15+
: never; // Should never happen
16+
17+
export {};

test-d/exclusify-union.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {expectType} from 'tsd';
2+
import type {ExclusifyUnion} from '../source/exclusify-union.d.ts';
3+
import type {NonRecursiveType} from '../source/internal/type.d.ts';
4+
5+
expectType<{a: string; b?: never} | {a?: never; b: number}>({} as ExclusifyUnion<{a: string} | {b: number}>);
6+
expectType<{a: string; b?: never; c?: never} | {a?: never; b: number; c?: never} | {a?: never; b?: never; c: boolean}>(
7+
{} as ExclusifyUnion<{a: string} | {b: number} | {c: boolean}>,
8+
);
9+
expectType<{a: string; b: number; c?: never; d?: never} | {a?: never; b?: never; c: string; d: number}>(
10+
{} as ExclusifyUnion<{a: string; b: number} | {c: string; d: number}>,
11+
);
12+
expectType<
13+
| {a: string; b?: never; c?: never; d?: never; e?: never; f?: never}
14+
| {a?: never; b: string; c: number; d?: never; e?: never; f?: never}
15+
| {a?: never; b?: never; c?: never; d: 1; e: 2; f: 3}
16+
>(
17+
{} as ExclusifyUnion<{a: string} | {b: string; c: number} | {d: 1; e: 2; f: 3}>,
18+
);
19+
20+
// Shared keys
21+
expectType<{a: string; b?: never} | {a: string; b: number}>({} as ExclusifyUnion<{a: string} | {a: string; b: number}>);
22+
expectType<{a: string; b?: never; c?: never} | {a: string; b: number; c?: never} | {a?: never; b: string; c: boolean}>(
23+
{} as ExclusifyUnion<{a: string} | {a: string; b: number} | {b: string; c: boolean}>,
24+
);
25+
26+
// Already exclusive unions
27+
expectType<{a: string; b?: never} | {a?: never; b: number}>({} as ExclusifyUnion<{a: string; b?: never} | {a?: never; b: number}>);
28+
expectType<{a: string} | {a: number}>({} as ExclusifyUnion<{a: string} | {a: number}>);
29+
30+
// Preserves property modifiers
31+
expectType<{a?: 1; readonly b: 2; readonly c?: 3; d?: never; e?: never} | {a?: never; b?: never; c?: never; d: 4; readonly e?: 5}>(
32+
{} as ExclusifyUnion<{a?: 1; readonly b: 2; readonly c?: 3} | {d: 4; readonly e?: 5}>,
33+
);
34+
expectType<{a?: string; readonly b: number} | {readonly a: string; b?: number}>(
35+
{} as ExclusifyUnion<{a?: string; readonly b: number} | {readonly a: string; b?: number}>,
36+
);
37+
38+
// Non-recursive types
39+
expectType<Set<string> | Map<string, string>>({} as ExclusifyUnion<Set<string> | Map<string, string>>);
40+
expectType<string[] | Set<string>>({} as ExclusifyUnion<string[] | Set<string>>);
41+
expectType<NonRecursiveType>({} as ExclusifyUnion<NonRecursiveType>);
42+
43+
// Mix of non-recursive and recursive types
44+
expectType<{a: string; b?: never} | {a: number; b: true} | undefined>({} as ExclusifyUnion<{a: string} | {a: number; b: true} | undefined>);
45+
expectType<RegExp | {test: string}>({} as ExclusifyUnion<RegExp | {test: string}>);
46+
expectType<RegExp | null | {foo?: string; bar?: never; baz?: never} | {foo?: never; bar: number; baz: any}>(
47+
{} as ExclusifyUnion<RegExp | null | {foo?: string} | {bar: number; baz: any}>,
48+
);
49+
50+
// Boundary types
51+
expectType<any>({} as ExclusifyUnion<any>);
52+
expectType<never>({} as ExclusifyUnion<never>);

0 commit comments

Comments
 (0)