-
-
Notifications
You must be signed in to change notification settings - Fork 679
UnionToTuple: Fix behavior when a union member is a supertype of another; Add ExcludeExactly type
#1349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UnionToTuple: Fix behavior when a union member is a supertype of another; Add ExcludeExactly type
#1349
Changes from 15 commits
ccb31dd
397ebf4
167f3eb
08bc31c
b24f977
0dbb9c0
7ef927a
e95c1ee
3b54932
8b984c9
4baac7e
0699dc0
01cbb49
408b2de
614ad2a
e4bd1e8
65cc0bd
68c4798
78fcde9
6822834
5dce64e
9579825
2d728ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type {IsNever} from './is-never.d.ts'; | ||
| import type {IsAny} from './is-any.d.ts'; | ||
| import type {If} from './if.d.ts'; | ||
| import type {IfNotAnyOrNever, SimpleIsEqual} from './internal/type.d.ts'; | ||
|
|
||
| /** | ||
| A stricter version of `Exclude<T, U>` that ensures objects with different key modifiers are not considered identical. | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| TypeScript's built-in `Exclude` and `ExcludeStrict` in `type-fest` don't distinguish key modifiers of objects. | ||
|
|
||
| @example | ||
| ``` | ||
| import type {ExcludeStrict} from 'type-fest'; | ||
|
|
||
| type NeverReturned_0 = Exclude<{a: 0} | {readonly a: 0}, {readonly a: 0}>; | ||
| //=> never | ||
| type NeverReturned_1 = ExcludeStrict<{a: 0} | {readonly a: 0}, {readonly a: 0}>; | ||
| //=> never | ||
| ``` | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| `ExcludeExactly` keeps the union members element if the members are not identical. | ||
|
|
||
| @example | ||
| ``` | ||
| import type {ExcludeExactly} from 'type-fest'; | ||
|
|
||
| type ExcludeNever = ExcludeExactly<{a: 0} | {a: 0} | {readonly a: 0}, never>; | ||
| //=> {a: 0} | {a: 0} | {readonly a: 0} | ||
| type ExcludeReadonlyKey = ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>; | ||
| //=> {a: 0} | ||
| type ExcludeKey = ExcludeExactly<{readonly a: 0}, {a: 0}>; | ||
| //=> {readonly a: 0} | ||
| type ExcludeReadonly = ExcludeExactly<{readonly a: 0}, {readonly a: 0}>; | ||
| //=> never | ||
| type ExcludeSubType = ExcludeExactly<0 | 1 | number, 1>; | ||
| //=> number | ||
| type ExcludeAllSet = ExcludeExactly<0 | 1 | number, number>; | ||
| //=> never | ||
| type ExcludeFromUnknown = ExcludeExactly<unknown, string>; | ||
| //=> unknown | ||
| type ExcludeFromUnknownArray = ExcludeExactly<number[] | unknown[], number[]>; | ||
| //=> unknown[] | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| @category Improved Built-in | ||
| */ | ||
| export type ExcludeExactly<Union, Delete> = | ||
| IfNotAnyOrNever< | ||
| Union, | ||
| _ExcludeExactly<Union, Delete>, | ||
| // If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`. | ||
| If<IsAny<Delete>, never, Union>, | ||
| // If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`. | ||
| If<IsNever<Delete>, never, Union> | ||
| >; | ||
|
|
||
| type _ExcludeExactly<Union, Delete> = | ||
| IfNotAnyOrNever<Delete, | ||
| Union extends unknown // For distributing `Union` | ||
| ? [Delete extends unknown // For distributing `Delete` | ||
| ? If<SimpleIsEqual<Union, Delete>, true, never> | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| : never] extends [never] ? Union : never | ||
| : never, | ||
| // If `Delete` is `any` or `never`, then return `Union`, | ||
| // because `Union` cannot be `any` or `never` here. | ||
| Union, Union | ||
| >; | ||
|
|
||
| export {}; | ||
taiyakihitotsu marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import type {IsNever} from './is-never.d.ts'; | ||
| import type {UnionToIntersection} from './union-to-intersection.d.ts'; | ||
|
|
||
| /** | ||
| Return a member of a union type. Order is not guaranteed. | ||
| Returns `never` when the input is `never`. | ||
|
|
||
| @see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468375328 | ||
|
|
||
| Use-cases: | ||
| - Implementing recursive type functions that accept a union type. | ||
| - Reducing a union one member at a time, for example when building tuples. | ||
|
|
||
| It can detect a termination case using {@link IsNever `IsNever`}. | ||
|
|
||
| @example | ||
| ``` | ||
| import type {LastOfUnion, ExcludeExactly, IsNever} from 'type-fest'; | ||
|
|
||
| export type UnionToTuple<T, L = LastOfUnion<T>> = | ||
| IsNever<T> extends false | ||
| ? [...UnionToTuple<ExcludeExactly<T, L>>, L] | ||
| : []; | ||
| ``` | ||
|
|
||
| @example | ||
| ``` | ||
| import type {LastOfUnion} from 'type-fest'; | ||
|
|
||
| type Last = LastOfUnion<1 | 2 | 3>; | ||
| //=> 3 | ||
|
|
||
| type LastNever = LastOfUnion<never>; | ||
| //=> never | ||
| ``` | ||
|
|
||
| @category Type | ||
| */ | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| export type LastOfUnion<T> = | ||
| true extends IsNever<T> | ||
| ? never | ||
| : UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) | ||
| ? R | ||
| : never; | ||
|
|
||
| export {}; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please create another PR for the purpose of exposing
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep this PR only for fixing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Note:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is the expected workflow:
How does this look?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Didn't get this. Can't see
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the confusion, I meant this part here: https://github.com/sindresorhus/type-fest/pull/1368/changes#diff-6ce133a34fd79b9e6b57c337b69241c8a393399e40f7e37e64bba9e421d7c2b0R31 I've edited the comment to clear it. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,6 @@ | ||
| import type {IsNever} from './is-never.d.ts'; | ||
| import type {UnionToIntersection} from './union-to-intersection.d.ts'; | ||
|
|
||
| /** | ||
| Returns the last element of a union type. | ||
|
|
||
| @example | ||
| ``` | ||
| type Last = LastOfUnion<1 | 2 | 3>; | ||
| //=> 3 | ||
| ``` | ||
| */ | ||
| type LastOfUnion<T> = | ||
| UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) | ||
| ? R | ||
| : never; | ||
| import type {ExcludeExactly} from './exclude-exactly.d.ts'; | ||
| import type {LastOfUnion} from './last-of-union.d.ts'; | ||
|
|
||
| /** | ||
| Convert a union type into an unordered tuple type of its elements. | ||
|
|
@@ -52,7 +39,9 @@ const petList = Object.keys(pets) as UnionToTuple<Pet>; | |
| */ | ||
| export type UnionToTuple<T, L = LastOfUnion<T>> = | ||
| IsNever<T> extends false | ||
| ? [...UnionToTuple<Exclude<T, L>>, L] | ||
| ? ExcludeExactly<T, L> extends infer E // Improve performance. | ||
|
||
| ? [...UnionToTuple<E>, L] | ||
| : never // Unreachable. | ||
| : []; | ||
|
|
||
| export {}; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import {expectType} from 'tsd'; | ||
| import type {ExcludeExactly} from '../index.d.ts'; | ||
|
|
||
| expectType<number>({} as ExcludeExactly<0 | 1 | number, '1'>); | ||
taiyakihitotsu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| expectType<never>({} as ExcludeExactly<0 | 1 | number, number>); | ||
| expectType<0>({} as ExcludeExactly<0, number>); | ||
| expectType<string>({} as ExcludeExactly<'0' | '1' | string, '1'>); | ||
| expectType<never>({} as ExcludeExactly<'0' | '1' | string, string>); | ||
|
|
||
| // `{readonly a: t}` should not be equal to `{a: t}` because of assignability. | ||
| expectType<{a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>); | ||
| expectType<{readonly a: 0}>({} as ExcludeExactly<{readonly a: 0}, {a: 0}>); | ||
| expectType<never>({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>); | ||
|
|
||
| // `never` does nothing. | ||
| expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>); | ||
| expectType<never>({} as ExcludeExactly<never, never>); | ||
som-sm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Edge cases. | ||
| expectType<never>({} as ExcludeExactly<never, never>); | ||
| expectType<any>({} as ExcludeExactly<any, never>); | ||
| expectType<unknown>({} as ExcludeExactly<unknown, never>); | ||
|
|
||
| // `unknown` cannot be excluded like `unknown\T` in any cases. | ||
| expectType<unknown>({} as ExcludeExactly<unknown, string>); | ||
| expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>); | ||
| expectType<unknown[]>({} as ExcludeExactly<unknown[], number[]>); | ||
| expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>); | ||
| expectType<unknown[]>({} as ExcludeExactly<number[] | unknown[], number[]>); | ||
|
|
||
| // `unknown` excludes `unknown`, `any` excludes `any`. | ||
| expectType<never>({} as ExcludeExactly<unknown, unknown>); | ||
| expectType<unknown>({} as ExcludeExactly<unknown, any>); | ||
| expectType<never>({} as ExcludeExactly<any, any>); | ||
| expectType<any>({} as ExcludeExactly<any, unknown>); | ||
| expectType<string | number>({} as ExcludeExactly<string | number, unknown>); | ||
| expectType<string | number>({} as ExcludeExactly<string | number, any>); | ||
|
|
||
| // Union | ||
| expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); | ||
| expectType<never>({} as ExcludeExactly<0 | 1 | 2, 0 | 1 | 2>); | ||
| expectType<{readonly a?: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0}>); | ||
| expectType<never>({} as ExcludeExactly<{a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}>); | ||
|
|
||
| // Identical Union | ||
| expectType<never>({} as ExcludeExactly<{a: 0} | {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
|
|
||
| // Identical Intersection | ||
| expectType<never>({} as ExcludeExactly<{a: 0} & {a: 0}, {a: 0}>); // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import {expectType} from 'tsd'; | ||
| import type {TupleOf} from '../../index.d.ts'; | ||
| import type {SimpleIsEqual} from '../../source/internal/index.d.ts'; | ||
|
|
||
| expectType<false>({} as SimpleIsEqual<number, string>); | ||
| expectType<true>({} as SimpleIsEqual<1, 1>); | ||
| expectType<false>({} as SimpleIsEqual<'A', 'B'>); | ||
| expectType<true>({} as SimpleIsEqual<'foo', 'foo'>); | ||
| expectType<false>({} as SimpleIsEqual<true, false>); | ||
| expectType<true>({} as SimpleIsEqual<false, false>); | ||
|
|
||
| expectType<false>({} as SimpleIsEqual<any, number>); | ||
| expectType<false>({} as SimpleIsEqual<'', never>); | ||
| expectType<true>({} as SimpleIsEqual<any, any>); | ||
| expectType<true>({} as SimpleIsEqual<never, never>); | ||
| expectType<false>({} as SimpleIsEqual<any, never>); | ||
| expectType<false>({} as SimpleIsEqual<never, any>); | ||
| expectType<false>({} as SimpleIsEqual<any, unknown>); | ||
| // `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. | ||
| expectType<true>({} as SimpleIsEqual<never, unknown>); | ||
| // `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. | ||
| expectType<true>({} as SimpleIsEqual<unknown, never>); | ||
| expectType<false>({} as SimpleIsEqual<[never], [unknown]>); | ||
| expectType<false>({} as SimpleIsEqual<[unknown], [never]>); | ||
| expectType<false>({} as SimpleIsEqual<[any], [never]>); | ||
| expectType<true>({} as SimpleIsEqual<[any], [any]>); | ||
| expectType<true>({} as SimpleIsEqual<[never], [never]>); | ||
|
|
||
| expectType<false>({} as SimpleIsEqual<1 | 2, 1>); | ||
| expectType<false>({} as SimpleIsEqual<1 | 2, 2 | 3>); | ||
| expectType<true>({} as SimpleIsEqual<1 | 2, 2 | 1>); | ||
| expectType<false>({} as SimpleIsEqual<boolean, true>); | ||
|
|
||
| expectType<true>({} as SimpleIsEqual<{a: 1}, {a: 1}>); | ||
| expectType<false>({} as SimpleIsEqual<{a: 1}, {a?: 1}>); | ||
| expectType<false>({} as SimpleIsEqual<{a: 1}, {readonly a: 1}>); | ||
|
|
||
| expectType<true>({} as SimpleIsEqual<[], []>); | ||
| expectType<true>({} as SimpleIsEqual<readonly [], readonly []>); | ||
| expectType<false>({} as SimpleIsEqual<readonly [], []>); | ||
| expectType<true>({} as SimpleIsEqual<number[], number[]>); | ||
| expectType<true>({} as SimpleIsEqual<readonly number[], readonly number[]>); | ||
| expectType<false>({} as SimpleIsEqual<readonly number[], number[]>); | ||
| expectType<true>({} as SimpleIsEqual<[string], [string]>); | ||
| expectType<false>({} as SimpleIsEqual<[string], [string, number]>); | ||
| expectType<false>({} as SimpleIsEqual<[0, 1] | [0, 2], [0, 2]>); | ||
|
|
||
| type LongTupleNumber = TupleOf<50, 0>; | ||
| expectType<true>({} as SimpleIsEqual<LongTupleNumber, LongTupleNumber>); | ||
|
|
||
| type ReadonlyLongTupleNumber = Readonly<TupleOf<50, 0>>; | ||
| expectType<true>({} as SimpleIsEqual<ReadonlyLongTupleNumber, ReadonlyLongTupleNumber>); | ||
|
|
||
| expectType<false>({} as SimpleIsEqual<ReadonlyLongTupleNumber, LongTupleNumber>); | ||
|
|
||
| // Missing all generic parameters. | ||
| // @ts-expect-error | ||
| type A = SimpleIsEqual; | ||
|
|
||
| // Missing `Y` generic parameter. | ||
| // @ts-expect-error | ||
| type B = SimpleIsEqual<number>; | ||
|
|
||
| // Test for issue https://github.com/sindresorhus/type-fest/issues/537 | ||
| type UnionType = SimpleIsEqual<{a: 1} | {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
| expectType<UnionType>(true); | ||
|
|
||
| type IntersectionType = SimpleIsEqual<{a: 1} & {a: 1}, {a: 1}>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
| expectType<IntersectionType>(true); | ||
|
|
||
| // Test for PR https://github.com/sindresorhus/type-fest/pull/1231 | ||
| type BranchOnWrappedTupleMatches<Tpl> = (Tpl extends [[0, 2]] ? 'Foo' : 'Bar'); | ||
| type BranchOnWrappedTupleDoesNotMatch<Tpl> = (Tpl extends [[0, 1]] ? 'Foo' : 'Bar'); | ||
| type BranchOnTupleMatches<Tpl> = (Tpl extends [0, 2] ? 'Foo' : 'Bar'); | ||
| type BranchOnTupleDoesNotMatch<Tpl> = (Tpl extends [0, 1] ? 'Foo' : 'Bar'); | ||
|
|
||
| declare const equalWrappedTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnWrappedTupleMatches<[[0, 2]]> & BranchOnWrappedTupleDoesNotMatch<[[0, 2]]>), never>; | ||
| expectType<true>(equalWrappedTupleIntersectionToBeNeverAndNever); | ||
|
|
||
| // `IsEqual` returns `false`, `SimpleIsEqual` returns `true`. | ||
| declare const equalWrappedTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnWrappedTupleMatches<[Tpl]> & BranchOnWrappedTupleDoesNotMatch<[Tpl]>), never> : never; | ||
| expectType<false>(equalWrappedTupleIntersectionToBeNeverAndNeverExpanded); | ||
|
|
||
| declare const equalTupleIntersectionToBeNeverAndNever: SimpleIsEqual<(BranchOnTupleMatches<[0, 2]> & BranchOnTupleDoesNotMatch<[0, 2]>), never>; | ||
| expectType<true>(equalTupleIntersectionToBeNeverAndNever); | ||
|
|
||
| declare const equalTupleIntersectionToBeNeverAndNeverExpanded: [0, 2] extends infer Tpl ? SimpleIsEqual<(BranchOnTupleMatches<Tpl> & BranchOnTupleDoesNotMatch<Tpl>), never> : never; | ||
| expectType<true>(equalTupleIntersectionToBeNeverAndNeverExpanded); | ||
|
|
||
| declare const equalTupleIntersectionAndTuple: SimpleIsEqual<[{a: 1}] & [{a: 1}], [{a: 1}]>; // eslint-disable-line @typescript-eslint/no-duplicate-type-constituents | ||
| expectType<true>(equalTupleIntersectionAndTuple); | ||
|
|
||
| // Test for Issue https://github.com/sindresorhus/type-fest/issues/1305 | ||
| type Assignability<T, U, _V extends SimpleIsEqual<T, U>> = any; | ||
| type TestAssignability<T> = Assignability<T, T, true>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this implementation simpler/better? It passes all the existing tests and also works as suggested with
anyandunknown.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your definition doesn't use
LastOfUnionand is clearer about the comparison part, since theSimpleIsEqualpattern is well-known, and all tests pass.But
SimpleIsEqualis already defined inis-equal.d.ts, so should we move it to internal, or even export it? Is that ok?(I vaguely think it is acceptable because this comparison construct is useful to define some type functions.)
And in this situation,
LastOfUnionis still worth exporting as a standalone utility.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1349 (comment)