diff --git a/index.d.ts b/index.d.ts index 8a9926d8d..d9d9973bf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -207,5 +207,6 @@ export type {TsConfigJson} from './source/tsconfig-json.d.ts'; export type {ExtendsStrict} from './source/extends-strict.d.ts'; export type {ExtractStrict} from './source/extract-strict.d.ts'; export type {ExcludeStrict} from './source/exclude-strict.d.ts'; +export type {ExcludeExactly} from './source/exclude-exactly.d.ts'; export {}; diff --git a/readme.md b/readme.md index 47e9fee30..8d76982db 100644 --- a/readme.md +++ b/readme.md @@ -319,6 +319,7 @@ Click the type names for complete docs. - [`ExtendsStrict`](source/extends-strict.d.ts) - A stricter, non-distributive version of `extends` for checking whether one type is assignable to another. - [`ExtractStrict`](source/extract-strict.d.ts) - A stricter version of `Extract` that ensures every member of `U` can successfully extract something from `T`. - [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude` that ensures every member of `U` can successfully exclude something from `T`. +- [`ExcludeExactly`](source/exclude-exactly.d.ts) - A stricter version of `Exclude` that excludes types only when they are exactly identical. ## Declined types diff --git a/source/exclude-exactly.d.ts b/source/exclude-exactly.d.ts new file mode 100644 index 000000000..5ab98c4fa --- /dev/null +++ b/source/exclude-exactly.d.ts @@ -0,0 +1,57 @@ +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 {IsEqual} from './is-equal.d.ts'; +import type {IfNotAnyOrNever} from './internal/type.d.ts'; + +/** +A stricter version of `Exclude` that excludes types only when they are exactly identical. + +@example +``` +import type {ExcludeExactly} from 'type-fest'; + +type TestExclude1 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, string>; +//=> 1 | 2 | 3 + +type TestExcludeExactly1 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, string>; +//=> 'a' | 'b' | 'c' | 1 | 2 | 3 + +type TestExclude2 = Exclude<'a' | 'b' | 'c' | 1 | 2 | 3, any>; +//=> never + +type TestExcludeExactly2 = ExcludeExactly<'a' | 'b' | 'c' | 1 | 2 | 3, any>; +//=> 'a' | 'b' | 'c' | 1 | 2 | 3 + +type TestExclude3 = Exclude<{a: string} | {a: string; b: string}, {a: string}>; +//=> never + +type TestExcludeExactly3 = ExcludeExactly<{a: string} | {a: string; b: string}, {a: string}>; +//=> {a: string; b: string} +``` + +@category Improved Built-in +*/ +export type ExcludeExactly = + IfNotAnyOrNever< + Union, + _ExcludeExactly, + // If `Union` is `any`, then if `Delete` is `any`, return `never`, else return `Union`. + If, never, Union>, + // If `Union` is `never`, then if `Delete` is `never`, return `never`, else return `Union`. + If, never, Union> + >; + +type _ExcludeExactly = + IfNotAnyOrNever, true, never> + : 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 {}; diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index c1f53a2b9..377c456cb 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -3,6 +3,7 @@ import type {IsAny} from '../is-any.d.ts'; import type {IsNever} from '../is-never.d.ts'; import type {Primitive} from '../primitive.d.ts'; import type {UnknownArray} from '../unknown-array.d.ts'; +import type {UnionToIntersection} from '../union-to-intersection.d.ts'; /** Matches any primitive, `void`, `Date`, or `RegExp` value. diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index b0183048d..306b4687e 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -1,4 +1,3 @@ -import type {IsNever} from './is-never.d.ts'; /** Returns a boolean for whether the two given types are equal. diff --git a/source/union-to-tuple.d.ts b/source/union-to-tuple.d.ts index 5a6d13ae2..a8734077a 100644 --- a/source/union-to-tuple.d.ts +++ b/source/union-to-tuple.d.ts @@ -1,3 +1,4 @@ +import type {ExcludeExactly} from './exclude-exactly.d.ts'; import type {IsNever} from './is-never.d.ts'; import type {UnionToIntersection} from './union-to-intersection.d.ts'; @@ -52,7 +53,7 @@ const petList = Object.keys(pets) as UnionToTuple; */ export type UnionToTuple> = IsNever extends false - ? [...UnionToTuple>, L] + ? [...UnionToTuple>, L] : []; export {}; diff --git a/test-d/exclude-exactly.ts b/test-d/exclude-exactly.ts new file mode 100644 index 000000000..4df36e8a9 --- /dev/null +++ b/test-d/exclude-exactly.ts @@ -0,0 +1,51 @@ +import {expectType} from 'tsd'; +import type {ExcludeExactly} from '../index.d.ts'; + +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType<0>({} as ExcludeExactly<0, number>); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType<'0'>({} as ExcludeExactly<'0', string>); + +expectType<{a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {readonly a: 0}>); +expectType<{readonly a: 0}>({} as ExcludeExactly<{a: 0} | {readonly a: 0}, {a: 0}>); +expectType({} as ExcludeExactly<{readonly a: 0}, {readonly a: 0}>); + +// `never` excludes nothing +expectType<0 | 1 | 2>({} as ExcludeExactly<0 | 1 | 2, never>); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); + +// Excluding from `unknown`/`any` +expectType({} as ExcludeExactly); +expectType<[unknown]>({} as ExcludeExactly<[unknown], [number]>); +expectType({} as ExcludeExactly); +expectType<{a: unknown}>({} as ExcludeExactly<{a: unknown}, {a: number}>); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType<[any]>({} as ExcludeExactly<[any], [number]>); +expectType({} as ExcludeExactly); +expectType<{a: any}>({} as ExcludeExactly<{a: any}, {a: number}>); +expectType({} as ExcludeExactly); + +// Excluding `unknown`/`any` +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); +expectType({} as ExcludeExactly); + +// Unions +expectType<2>({} as ExcludeExactly<0 | 1 | 2, 0 | 1>); +expectType({} 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({} as ExcludeExactly< + {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0}, {a: 0} | {readonly a: 0} | {a?: 0} | {readonly a?: 0} +>); diff --git a/test-d/union-to-tuple.ts b/test-d/union-to-tuple.ts index 12de3cf2b..7a2eb51e4 100644 --- a/test-d/union-to-tuple.ts +++ b/test-d/union-to-tuple.ts @@ -11,3 +11,15 @@ expectType({} as (1 | 2 | 3)); type Options2 = UnionToTuple; expectType({} as (1 | false | true)); + +// Test for https://github.com/sindresorhus/type-fest/issues/1352 +// This union is special because `{readonly a: 0}` extends `{a: 0}`, and `{a: 0}` also extends `{readonly a: 0}`, +// meaning both types are assignable to each other. +// See [this comment](https://github.com/sindresorhus/type-fest/pull/1349#issuecomment-3858719735) for more details. +type DifferentModifierUnion = {readonly a: 0} | {a: 0}; +expectType({} as UnionToTuple[number]); + +// Edge cases. +expectType<[]>({} as UnionToTuple); +expectType<[any]>({} as UnionToTuple); +expectType<[unknown]>({} as UnionToTuple);