Skip to content

Commit 0f923d0

Browse files
taiyakihitotsusindresorhussom-sm
authored
UnionToTuple: Fix behavior when a union member is a supertype of another; Add ExcludeExactly type (#1349)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: Som Shekhar Mukherjee <iamssmkhrj@gmail.com>
1 parent 94aa3f8 commit 0f923d0

File tree

8 files changed

+125
-2
lines changed

8 files changed

+125
-2
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,6 @@ export type {TsConfigJson} from './source/tsconfig-json.d.ts';
213213
export type {ExtendsStrict} from './source/extends-strict.d.ts';
214214
export type {ExtractStrict} from './source/extract-strict.d.ts';
215215
export type {ExcludeStrict} from './source/exclude-strict.d.ts';
216+
export type {ExcludeExactly} from './source/exclude-exactly.d.ts';
216217

217218
export {};

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ Click the type names for complete docs.
325325
- [`ExtendsStrict`](source/extends-strict.d.ts) - A stricter, non-distributive version of `extends` for checking whether one type is assignable to another.
326326
- [`ExtractStrict`](source/extract-strict.d.ts) - A stricter version of `Extract<T, U>` that ensures every member of `U` can successfully extract something from `T`.
327327
- [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude<T, U>` that ensures every member of `U` can successfully exclude something from `T`.
328+
- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude<T, U>` that excludes types only when they are exactly identical.
328329

329330
## Declined types
330331

source/exclude-exactly.d.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type {IsNever} from './is-never.d.ts';
2+
import type {IsAny} from './is-any.d.ts';
3+
import type {If} from './if.d.ts';
4+
import type {IsEqual} from './is-equal.d.ts';
5+
import type {IfNotAnyOrNever} from './internal/type.d.ts';
6+
7+
/**
8+
A stricter version of `Exclude<T, U>` that excludes types only when they are exactly identical.
9+
10+
@example
11+
```
12+
import type {ExcludeExactly} from 'type-fest';
13+
14+
type TestExclude1 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, string>;
15+
//=> 1 | 2 | 3
16+
17+
type TestExcludeExactly1 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, string>;
18+
//=> 'a' | 'b' | 'c' | 1 | 2 | 3
19+
20+
type TestExclude2 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, any>;
21+
//=> never
22+
23+
type TestExcludeExactly2 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, any>;
24+
//=> 'a' | 'b' | 'c' | 1 | 2 | 3
25+
26+
type TestExclude3 = Exclude<{a: string} | {a: string; b: string}, {a: string}>;
27+
//=> never
28+
29+
type TestExcludeExactly3 = ExcludeExactly<{a: string} | {a: string; b: string}, {a: string}>;
30+
//=> {a: string; b: string}
31+
```
32+
33+
@category Improved Built-in
34+
*/
35+
export type ExcludeExactly<Union, Delete> =
36+
IfNotAnyOrNever<
37+
Union,
38+
_ExcludeExactly<Union, Delete>,
39+
// If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`.
40+
If<IsAny<Delete>, never, Union>,
41+
// If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`.
42+
If<IsNever<Delete>, never, Union>
43+
>;
44+
45+
type _ExcludeExactly<Union, Delete> =
46+
IfNotAnyOrNever<Delete,
47+
Union extends unknown // For distributing `Union`
48+
? [Delete extends unknown // For distributing `Delete`
49+
? If<IsEqual<Union, Delete>, true, never>
50+
: never] extends [never] ? Union : never
51+
: never,
52+
// If `Delete` is `any` or `never`, then return `Union`,
53+
// because `Union` cannot be `any` or `never` here.
54+
Union, Union
55+
>;
56+
57+
export {};

source/internal/type.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {IsAny} from '../is-any.d.ts';
33
import type {IsNever} from '../is-never.d.ts';
44
import type {Primitive} from '../primitive.d.ts';
55
import type {UnknownArray} from '../unknown-array.d.ts';
6+
import type {UnionToIntersection} from '../union-to-intersection.d.ts';
67

78
/**
89
Matches any primitive, `void`, `Date`, or `RegExp` value.

source/is-equal.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type {IsNever} from './is-never.d.ts';
21
/**
32
Returns a boolean for whether the two given types are equal.
43

source/union-to-tuple.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {ExcludeExactly} from './exclude-exactly.d.ts';
12
import type {IsNever} from './is-never.d.ts';
23
import type {UnionMember} from './union-member.d.ts';
34

@@ -38,7 +39,7 @@ const petList = Object.keys(pets) as UnionToTuple<Pet>;
3839
*/
3940
export type UnionToTuple<T, L = UnionMember<T>> =
4041
IsNever<T> extends false
41-
? [...UnionToTuple<Exclude<T, L>>, L]
42+
? [...UnionToTuple<ExcludeExactly<T, L>>, L]
4243
: [];
4344

4445
export {};

test-d/exclude-exactly.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {expectType} from 'tsd';
2+
import type {ExcludeExactly} from '../index.d.ts';
3+
4+
expectType<number>({} as ExcludeExactly<number, '1'>);
5+
expectType<never>({} as ExcludeExactly<number, number>);
6+
expectType<0>({} as ExcludeExactly<0, number>);
7+
expectType<string>({} as ExcludeExactly<string, '1'>);
8+
expectType<never>({} as ExcludeExactly<string, string>);
9+
expectType<'0'>({} as ExcludeExactly<'0', string>);
10+
11+
expectType<{a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>);
12+
expectType<{readonly a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {a: 0}>);
13+
expectType<never>({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>);
14+
15+
// `never` excludes nothing
16+
expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>);
17+
expectType<never>({} as ExcludeExactly<never, never>);
18+
expectType<any>({} as ExcludeExactly<any, never>);
19+
expectType<unknown>({} as ExcludeExactly<unknown, never>);
20+
21+
// Excluding from `unknown`/`any`
22+
expectType<unknown>({} as ExcludeExactly<unknown, string>);
23+
expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>);
24+
expectType<unknown[]>({} as ExcludeExactly<unknown[], number[]>);
25+
expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>);
26+
expectType<unknown[]>({} as ExcludeExactly<number[] | unknown[], number[]>);
27+
expectType<any>({} as ExcludeExactly<any, string>);
28+
expectType<[any]>({} as ExcludeExactly<[any], [number]>);
29+
expectType<any[]>({} as ExcludeExactly<any[], number[]>);
30+
expectType<{a: any}>({} as ExcludeExactly<{a: any}, {a: number}>);
31+
expectType<any[]>({} as ExcludeExactly<number[] | any[], number[]>);
32+
33+
// Excluding `unknown`/`any`
34+
expectType<never>({} as ExcludeExactly<unknown, unknown>);
35+
expectType<never>({} as ExcludeExactly<any, any>);
36+
expectType<unknown>({} as ExcludeExactly<unknown, any>);
37+
expectType<any>({} as ExcludeExactly<any, unknown>);
38+
expectType<string | number>({} as ExcludeExactly<string | number, unknown>);
39+
expectType<string | number>({} as ExcludeExactly<string | number, any>);
40+
expectType<never>({} as ExcludeExactly<never, any>);
41+
expectType<never>({} as ExcludeExactly<never, unknown>);
42+
43+
// Unions
44+
expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>);
45+
expectType<never>({} as ExcludeExactly<0 | 1 | 2, 0 | 1 | 2>);
46+
expectType<{readonly a?: 0}>({} as ExcludeExactly<
47+
{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0}
48+
>);
49+
expectType<never>({} as ExcludeExactly<
50+
{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}
51+
>);

test-d/union-to-tuple.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,15 @@ expectType<Options1[number]>({} as (1 | 2 | 3));
1111

1212
type Options2 = UnionToTuple<boolean | 1>;
1313
expectType<Options2[number]>({} as (1 | false | true));
14+
15+
// Test for https://github.com/sindresorhus/type-fest/issues/1352
16+
// This union is special because `{readonly a: 0}` extends `{a: 0}`, and `{a: 0}` also extends `{readonly a: 0}`,
17+
// meaning both types are assignable to each other.
18+
// See [this comment](https://github.com/sindresorhus/type-fest/pull/1349#issuecomment-3858719735) for more details.
19+
type DifferentModifierUnion = {readonly a: 0} | {a: 0};
20+
expectType<DifferentModifierUnion>({} as UnionToTuple<DifferentModifierUnion>[number]);
21+
22+
// Edge cases.
23+
expectType<[]>({} as UnionToTuple<never>);
24+
expectType<[any]>({} as UnionToTuple<any>);
25+
expectType<[unknown]>({} as UnionToTuple<unknown>);

0 commit comments

Comments
 (0)