From ff9a622f755e893337493fb3d8a13c6a7320621f Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 3 Dec 2024 16:35:55 +1300 Subject: [PATCH 1/8] feat: create AllUnionFields type --- index.d.ts | 1 + readme.md | 1 + source/all-union-fields.d.ts | 150 ++++++++++++++++ source/shared-union-fields.d.ts | 1 + test-d/all-union-fields.ts | 307 ++++++++++++++++++++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 source/all-union-fields.d.ts create mode 100644 test-d/all-union-fields.ts diff --git a/index.d.ts b/index.d.ts index ff20fbd95..627897589 100644 --- a/index.d.ts +++ b/index.d.ts @@ -128,6 +128,7 @@ export type {ArraySplice} from './source/array-splice'; export type {ArrayTail} from './source/array-tail'; export type {SetFieldType} from './source/set-field-type'; export type {Paths} from './source/paths'; +export type {AllUnionFields} from './source/all-union-fields'; export type {SharedUnionFields} from './source/shared-union-fields'; export type {SharedUnionFieldsDeep} from './source/shared-union-fields-deep'; export type {IsNull} from './source/is-null'; diff --git a/readme.md b/readme.md index 7bc311371..a5e971368 100644 --- a/readme.md +++ b/readme.md @@ -202,6 +202,7 @@ Click the type names for complete docs. - [`Paths`](source/paths.d.ts) - Generate a union of all possible paths to properties in the given object. - [`SharedUnionFields`](source/shared-union-fields.d.ts) - Create a type with shared fields from a union of object types. - [`SharedUnionFieldsDeep`](source/shared-union-fields-deep.d.ts) - Create a type with shared fields from a union of object types, deeply traversing nested structures. +- [`AllUnionFields`](source/all-union-fields.d.ts) - Create a type with all fields from a union of object types. - [`DistributedOmit`](source/distributed-omit.d.ts) - Omits keys from a type, distributing the operation over a union. - [`DistributedPick`](source/distributed-pick.d.ts) - Picks keys from a type, distributing the operation over a union. - [`And`](source/and.d.ts) - Returns a boolean for whether two given types are both true. diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts new file mode 100644 index 000000000..354685a8c --- /dev/null +++ b/source/all-union-fields.d.ts @@ -0,0 +1,150 @@ +import type {IsUnion, NonRecursiveType} from './internal'; +import type {ReadonlyKeysOf} from './readonly-keys-of'; +import type {SharedUnionFields} from './shared-union-fields'; +import type {Simplify} from './simplify'; +import type {TupleToUnion} from './tuple-to-union'; +import type {UnionToTuple} from './union-to-tuple'; +import type {UnknownArray} from './unknown-array'; + +/** +AllUnionFields options. + +@see {@link AllUnionFields} +*/ +export type AllUnionFieldsOptions = { + /** + When set to true, we assume no properties other than what is declared on each object in the union exist. + + @default false + */ + exact?: boolean; +}; + +/** +Create a type with all fields from a union of object types. + +Use-cases: +- You want a safe object type where each key exists in the union object. + +@example +``` +import type {AllUnionFields} from 'type-fest'; + +type Cat = { + name: string; + type: 'cat'; + catType: string; +}; + +type Dog = { + name: string; + type: 'dog'; + dogType: string; +}; + +function displayPetInfo(petInfo: Cat | Dog) { + // typeof petInfo => + // { + // name: string; + // type: 'cat'; + // catType: string; + // } | { + // name: string; + // type: 'dog'; + // dogType: string; + // } + + console.log('name: ', petInfo.name); + console.log('type: ', petInfo.type); + + // TypeScript complains about `catType` and `dogType` not existing on type `Cat | Dog`. + console.log('animal type: ', petInfo.catType ?? petInfo.dogType); +} + +function displayPetInfo(petInfo: AllUnionFields) { + // typeof petInfo => + // { + // name: string; + // type: 'cat' | 'dog'; + // catType?: unknown; + // dogType?: unknown; + // } + + console.log('name: ', petInfo.name); + console.log('type: ', petInfo.type); + + // No TypeScript error. + console.log('animal type: ', petInfo.catType ?? petInfo.dogType); +} +``` + +@see SharedUnionFields + +@category Object +@category Union +*/ +export type AllUnionFields< + Union, + Options extends AllUnionFieldsOptions = {exact: false}, +> = + // If `Union` is not a union type, return `Union` directly. + IsUnion extends false + ? Union + : // `Union extends` will convert `Union` + // to a [distributive conditionaltype](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types). + // But this is not what we want, so we need to wrap `Union` with `[]` to prevent it. + [Union] extends [ + | NonRecursiveType + | ReadonlyMap + | ReadonlySet + | UnknownArray, + ] + ? Union + : [Union] extends [object] + ? Simplify< + SharedUnionFields & + NonSharedUnionFields, keyof Union, Options> + > + : Union; + +type NonSharedUnionFields< + Tuple, + SharedKeys, + Options extends AllUnionFieldsOptions, +> = NonSharedUnionFieldsHelper< +Tuple, +Exclude, ReadonlyKeysOfEach | SharedKeys>, +Exclude, SharedKeys>, +Options +>; + +type NonSharedUnionFieldsHelper< + Tuple, + NonReadonlyKeys extends PropertyKey, + ReadonlyKeys extends PropertyKey, + Options extends AllUnionFieldsOptions, +> = { + [Key in NonReadonlyKeys]?: ValueOfEach; +} & { + readonly [Key in ReadonlyKeys]?: ValueOfEach; +}; + +type KeysOfEach = TupleToUnion<{ + [Index in keyof Tuple]: keyof Tuple[Index]; +}>; + +type ReadonlyKeysOfEach = TupleToUnion<{ + [Index in keyof Tuple]: ReadonlyKeysOf; +}>; + +type ValueOfEach< + Tuple, + Key, + Options extends AllUnionFieldsOptions, +> = Options['exact'] extends true + ? TupleToUnion<{ + [Index in keyof Tuple]: Key extends keyof Tuple[Index] + ? Tuple[Index][Key] + : never; + }> + : unknown; diff --git a/source/shared-union-fields.d.ts b/source/shared-union-fields.d.ts index 506f3696a..339a39389 100644 --- a/source/shared-union-fields.d.ts +++ b/source/shared-union-fields.d.ts @@ -59,6 +59,7 @@ function displayPetInfo(petInfo: SharedUnionFields) { ``` @see SharedUnionFieldsDeep +@see AllUnionFields @category Object @category Union diff --git a/test-d/all-union-fields.ts b/test-d/all-union-fields.ts new file mode 100644 index 000000000..fb2ee1722 --- /dev/null +++ b/test-d/all-union-fields.ts @@ -0,0 +1,307 @@ +import {expectType} from 'tsd'; +import {type AllUnionFields, type Simplify} from '../index'; + +type TestingType = { + function: () => void; + record: Record< + string, + { + propertyA: string; + } + >; + object: { + subObject: { + subSubObject: { + propertyA: string; + }; + }; + }; + string: string; + union: 'test1' | 'test2'; + number: number; + boolean: boolean; + date: Date; + regexp: RegExp; + symbol: symbol; + null: null; + undefined: undefined; + optional?: boolean | undefined; + readonly propertyWithKeyword: boolean; + map: Map; + set: Set; + objectSet: Set<{propertyA: string; propertyB: string}>; +}; + +type AllUnionFieldsExact = AllUnionFields; + +declare const normal: AllUnionFields< +TestingType | {string: string; number: number; foo: any} +>; +expectType +>>(normal); + +declare const normalExact: AllUnionFieldsExact< +TestingType | {string: string; number: number; foo: any} +>; +expectType> +>>(normalExact); + +declare const unMatched: AllUnionFields; +expectType>(unMatched); + +declare const unMatchedExact: AllUnionFieldsExact; +expectType +>>(unMatchedExact); + +declare const number: AllUnionFields; +expectType +>>(number); + +declare const numberExact: AllUnionFieldsExact; +expectType> +>>(numberExact); + +declare const string: AllUnionFields; +expectType +>>(string); + +declare const stringExact: AllUnionFieldsExact; +expectType> +>>(stringExact); + +declare const boolean: AllUnionFields; +expectType +>>(boolean); + +declare const booleanExact: AllUnionFieldsExact; +expectType> +>>(booleanExact); + +declare const date: AllUnionFields; +expectType +>>(date); + +declare const dateExact: AllUnionFieldsExact; +expectType> +>>(dateExact); + +declare const regexp: AllUnionFields; +expectType +>>(regexp); + +declare const regexpExact: AllUnionFieldsExact; +expectType> +>>(regexpExact); + +declare const symbol: AllUnionFields; +expectType +>>(symbol); + +declare const symbolExact: AllUnionFieldsExact; +expectType> +>>(symbolExact); + +declare const null_: AllUnionFields; +expectType +>>(null_); + +declare const nullExact: AllUnionFieldsExact; +expectType> +>>(nullExact); + +declare const undefined_: AllUnionFields; +expectType +>>(undefined_); + +declare const undefinedExact: AllUnionFieldsExact; +expectType> +>>(undefinedExact); + +declare const optional: AllUnionFields; +expectType +>>(optional); + +declare const optionalExact: AllUnionFieldsExact; +expectType> +>>(optionalExact); + +declare const propertyWithKeyword: AllUnionFields; +expectType +>>(propertyWithKeyword); + +declare const propertyWithKeywordExact: AllUnionFieldsExact; +expectType> +>>(propertyWithKeywordExact); + +declare const map: AllUnionFields; foo: any}>; +expectType; + foo?: unknown; +} & Omit<{[Key in keyof TestingType]?: unknown;}, 'map'> +>>(map); + +declare const mapExact: AllUnionFieldsExact; foo: any}>; +expectType; + foo?: any; +} & Partial> +>>(mapExact); + +declare const set: AllUnionFields; foo: any}>; +expectType; + foo?: unknown; +} & Omit<{[Key in keyof TestingType]?: unknown;}, 'set'> +>>(set); + +declare const setExact: AllUnionFieldsExact; foo: any}>; +expectType; + foo?: any; +} & Partial> +>>(setExact); + +declare const moreUnion: AllUnionFields; +expectType +>>(moreUnion); + +declare const moreUnionExact: AllUnionFieldsExact; +expectType> +>>(moreUnionExact); + +declare const union: AllUnionFields; +expectType +>>(union); + +declare const unionExact: AllUnionFieldsExact; +expectType> +>>(unionExact); + +declare const unionWithOptional: AllUnionFields<{a?: string; foo: number} | {a: string; bar: string}>; +expectType<{ + a?: string; + foo?: unknown; + bar?: unknown; +}>(unionWithOptional); + +declare const unionWithOptionalExact: AllUnionFieldsExact<{a?: string; foo: number} | {a: string; bar: string}>; +expectType<{ + a?: string; + foo?: number; + bar?: string; +}>(unionWithOptionalExact); + +declare const mixedKeywords: AllUnionFields<{readonly a: string; b: number} | {a: string; readonly b: string}>; +expectType<{ + readonly a: string; + readonly b: string | number; +}>(mixedKeywords); + +declare const mixedKeywordsExact: AllUnionFieldsExact<{readonly a: string; b: number} | {a: string; readonly b: string}>; +expectType<{ + readonly a: string; + readonly b: string | number; +}>(mixedKeywordsExact); + +declare const mixedKeywords2: AllUnionFields<{readonly a: string; b: number} | {a: string; readonly b: string} | {readonly c: number}>; +expectType<{ + readonly a?: unknown; + readonly b?: unknown; + readonly c?: unknown; +}>(mixedKeywords2); + +declare const mixedKeywords2Exact: AllUnionFieldsExact<{readonly a: string; b: number} | {a: string; readonly b: string} | {readonly c: number}>; +expectType<{ + readonly a?: string; + readonly b?: string | number; + readonly c?: number; +}>(mixedKeywords2Exact); From b7dda36bffa9ec82da75caf3e1a72d77f1369be3 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 4 Dec 2024 14:37:31 +1300 Subject: [PATCH 2/8] fix: remove `exact` option and just make that how it works --- source/all-union-fields.d.ts | 47 +++------- test-d/all-union-fields.ts | 174 ++++------------------------------- 2 files changed, 30 insertions(+), 191 deletions(-) diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts index 354685a8c..6a854ca8a 100644 --- a/source/all-union-fields.d.ts +++ b/source/all-union-fields.d.ts @@ -6,20 +6,6 @@ import type {TupleToUnion} from './tuple-to-union'; import type {UnionToTuple} from './union-to-tuple'; import type {UnknownArray} from './unknown-array'; -/** -AllUnionFields options. - -@see {@link AllUnionFields} -*/ -export type AllUnionFieldsOptions = { - /** - When set to true, we assume no properties other than what is declared on each object in the union exist. - - @default false - */ - exact?: boolean; -}; - /** Create a type with all fields from a union of object types. @@ -66,8 +52,8 @@ function displayPetInfo(petInfo: AllUnionFields) { // { // name: string; // type: 'cat' | 'dog'; - // catType?: unknown; - // dogType?: unknown; + // catType?: string; + // dogType?: string; // } console.log('name: ', petInfo.name); @@ -83,10 +69,7 @@ function displayPetInfo(petInfo: AllUnionFields) { @category Object @category Union */ -export type AllUnionFields< - Union, - Options extends AllUnionFieldsOptions = {exact: false}, -> = +export type AllUnionFields = // If `Union` is not a union type, return `Union` directly. IsUnion extends false ? Union @@ -103,30 +86,27 @@ export type AllUnionFields< : [Union] extends [object] ? Simplify< SharedUnionFields & - NonSharedUnionFields, keyof Union, Options> + NonSharedUnionFields, keyof Union> > : Union; type NonSharedUnionFields< Tuple, SharedKeys, - Options extends AllUnionFieldsOptions, > = NonSharedUnionFieldsHelper< Tuple, Exclude, ReadonlyKeysOfEach | SharedKeys>, -Exclude, SharedKeys>, -Options +Exclude, SharedKeys> >; type NonSharedUnionFieldsHelper< Tuple, NonReadonlyKeys extends PropertyKey, ReadonlyKeys extends PropertyKey, - Options extends AllUnionFieldsOptions, > = { - [Key in NonReadonlyKeys]?: ValueOfEach; + [Key in NonReadonlyKeys]?: ValueOfEach; } & { - readonly [Key in ReadonlyKeys]?: ValueOfEach; + readonly [Key in ReadonlyKeys]?: ValueOfEach; }; type KeysOfEach = TupleToUnion<{ @@ -140,11 +120,8 @@ type ReadonlyKeysOfEach = TupleToUnion<{ type ValueOfEach< Tuple, Key, - Options extends AllUnionFieldsOptions, -> = Options['exact'] extends true - ? TupleToUnion<{ - [Index in keyof Tuple]: Key extends keyof Tuple[Index] - ? Tuple[Index][Key] - : never; - }> - : unknown; +> = TupleToUnion<{ + [Index in keyof Tuple]: Key extends keyof Tuple[Index] + ? Tuple[Index][Key] + : never; +}>; diff --git a/test-d/all-union-fields.ts b/test-d/all-union-fields.ts index fb2ee1722..8d561db10 100644 --- a/test-d/all-union-fields.ts +++ b/test-d/all-union-fields.ts @@ -32,253 +32,128 @@ type TestingType = { objectSet: Set<{propertyA: string; propertyB: string}>; }; -type AllUnionFieldsExact = AllUnionFields; - declare const normal: AllUnionFields< TestingType | {string: string; number: number; foo: any} >; expectType ->>(normal); - -declare const normalExact: AllUnionFieldsExact< -TestingType | {string: string; number: number; foo: any} ->; -expectType> ->>(normalExact); +>>(normal); declare const unMatched: AllUnionFields; expectType>(unMatched); - -declare const unMatchedExact: AllUnionFieldsExact; -expectType ->>(unMatchedExact); +>>(unMatched); declare const number: AllUnionFields; -expectType ->>(number); - -declare const numberExact: AllUnionFieldsExact; expectType> ->>(numberExact); +>>(number); declare const string: AllUnionFields; -expectType ->>(string); - -declare const stringExact: AllUnionFieldsExact; expectType> ->>(stringExact); +>>(string); declare const boolean: AllUnionFields; -expectType ->>(boolean); - -declare const booleanExact: AllUnionFieldsExact; expectType> ->>(booleanExact); +>>(boolean); declare const date: AllUnionFields; -expectType ->>(date); - -declare const dateExact: AllUnionFieldsExact; expectType> ->>(dateExact); +>>(date); declare const regexp: AllUnionFields; -expectType ->>(regexp); - -declare const regexpExact: AllUnionFieldsExact; expectType> ->>(regexpExact); +>>(regexp); declare const symbol: AllUnionFields; -expectType ->>(symbol); - -declare const symbolExact: AllUnionFieldsExact; expectType> ->>(symbolExact); +>>(symbol); declare const null_: AllUnionFields; -expectType ->>(null_); - -declare const nullExact: AllUnionFieldsExact; expectType> ->>(nullExact); +>>(null_); declare const undefined_: AllUnionFields; -expectType ->>(undefined_); - -declare const undefinedExact: AllUnionFieldsExact; expectType> ->>(undefinedExact); +>>(undefined_); declare const optional: AllUnionFields; -expectType ->>(optional); - -declare const optionalExact: AllUnionFieldsExact; expectType> ->>(optionalExact); +>>(optional); declare const propertyWithKeyword: AllUnionFields; -expectType ->>(propertyWithKeyword); - -declare const propertyWithKeywordExact: AllUnionFieldsExact; expectType> ->>(propertyWithKeywordExact); +>>(propertyWithKeyword); declare const map: AllUnionFields; foo: any}>; -expectType; - foo?: unknown; -} & Omit<{[Key in keyof TestingType]?: unknown;}, 'map'> ->>(map); - -declare const mapExact: AllUnionFieldsExact; foo: any}>; expectType; foo?: any; } & Partial> ->>(mapExact); +>>(map); declare const set: AllUnionFields; foo: any}>; -expectType; - foo?: unknown; -} & Omit<{[Key in keyof TestingType]?: unknown;}, 'set'> ->>(set); - -declare const setExact: AllUnionFieldsExact; foo: any}>; expectType; foo?: any; } & Partial> ->>(setExact); +>>(set); declare const moreUnion: AllUnionFields; -expectType ->>(moreUnion); - -declare const moreUnionExact: AllUnionFieldsExact; expectType> ->>(moreUnionExact); +>>(moreUnion); declare const union: AllUnionFields; -expectType ->>(union); - -declare const unionExact: AllUnionFieldsExact; expectType> ->>(unionExact); +>>(union); declare const unionWithOptional: AllUnionFields<{a?: string; foo: number} | {a: string; bar: string}>; -expectType<{ - a?: string; - foo?: unknown; - bar?: unknown; -}>(unionWithOptional); - -declare const unionWithOptionalExact: AllUnionFieldsExact<{a?: string; foo: number} | {a: string; bar: string}>; expectType<{ a?: string; foo?: number; bar?: string; -}>(unionWithOptionalExact); +}>(unionWithOptional); declare const mixedKeywords: AllUnionFields<{readonly a: string; b: number} | {a: string; readonly b: string}>; expectType<{ @@ -286,22 +161,9 @@ expectType<{ readonly b: string | number; }>(mixedKeywords); -declare const mixedKeywordsExact: AllUnionFieldsExact<{readonly a: string; b: number} | {a: string; readonly b: string}>; -expectType<{ - readonly a: string; - readonly b: string | number; -}>(mixedKeywordsExact); - declare const mixedKeywords2: AllUnionFields<{readonly a: string; b: number} | {a: string; readonly b: string} | {readonly c: number}>; -expectType<{ - readonly a?: unknown; - readonly b?: unknown; - readonly c?: unknown; -}>(mixedKeywords2); - -declare const mixedKeywords2Exact: AllUnionFieldsExact<{readonly a: string; b: number} | {a: string; readonly b: string} | {readonly c: number}>; expectType<{ readonly a?: string; readonly b?: string | number; readonly c?: number; -}>(mixedKeywords2Exact); +}>(mixedKeywords2); From a157e8b32799730773a6010282edaf178adebb3d Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 15 Dec 2024 16:19:42 +1300 Subject: [PATCH 3/8] refactor: AllUnionFields --- source/all-union-fields.d.ts | 79 +++++++++--------------------------- 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts index 6a854ca8a..9415d6b60 100644 --- a/source/all-union-fields.d.ts +++ b/source/all-union-fields.d.ts @@ -1,9 +1,8 @@ -import type {IsUnion, NonRecursiveType} from './internal'; -import type {ReadonlyKeysOf} from './readonly-keys-of'; +import type {NonRecursiveType} from './internal'; +import type {IsEqual} from './is-equal'; +import type {KeysOfUnion} from './keys-of-union'; import type {SharedUnionFields} from './shared-union-fields'; import type {Simplify} from './simplify'; -import type {TupleToUnion} from './tuple-to-union'; -import type {UnionToTuple} from './union-to-tuple'; import type {UnknownArray} from './unknown-array'; /** @@ -69,59 +68,19 @@ function displayPetInfo(petInfo: AllUnionFields) { @category Object @category Union */ -export type AllUnionFields = - // If `Union` is not a union type, return `Union` directly. - IsUnion extends false - ? Union - : // `Union extends` will convert `Union` - // to a [distributive conditionaltype](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types). - // But this is not what we want, so we need to wrap `Union` with `[]` to prevent it. - [Union] extends [ - | NonRecursiveType - | ReadonlyMap - | ReadonlySet - | UnknownArray, - ] - ? Union - : [Union] extends [object] - ? Simplify< - SharedUnionFields & - NonSharedUnionFields, keyof Union> - > - : Union; - -type NonSharedUnionFields< - Tuple, - SharedKeys, -> = NonSharedUnionFieldsHelper< -Tuple, -Exclude, ReadonlyKeysOfEach | SharedKeys>, -Exclude, SharedKeys> ->; - -type NonSharedUnionFieldsHelper< - Tuple, - NonReadonlyKeys extends PropertyKey, - ReadonlyKeys extends PropertyKey, -> = { - [Key in NonReadonlyKeys]?: ValueOfEach; -} & { - readonly [Key in ReadonlyKeys]?: ValueOfEach; -}; - -type KeysOfEach = TupleToUnion<{ - [Index in keyof Tuple]: keyof Tuple[Index]; -}>; - -type ReadonlyKeysOfEach = TupleToUnion<{ - [Index in keyof Tuple]: ReadonlyKeysOf; -}>; - -type ValueOfEach< - Tuple, - Key, -> = TupleToUnion<{ - [Index in keyof Tuple]: Key extends keyof Tuple[Index] - ? Tuple[Index][Key] - : never; -}>; +export type AllUnionFields = [Union] extends [NonRecursiveType | ReadonlyMap | ReadonlySet | UnknownArray] + ? Union + : Simplify< + SharedUnionFields & + { + readonly [P in ReadonlyKeysOfUnion]?: ValueOfUnion; + } & { + [P in Exclude, ReadonlyKeysOfUnion | keyof Union>]?: ValueOfUnion; + } + >; + +type ValueOfUnion = Union extends unknown ? Key extends keyof Union ? Union[Key] : never : never; + +type ReadonlyKeysOfUnion = T extends unknown ? keyof { + [P in keyof T as IsEqual<{[Q in P]: T[P]}, {readonly [Q in P]: T[P]}> extends true ? P : never]: never +} : never; From b3b010b2c35ab70abc88d94f7089e3a99c04ccc2 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 15 Dec 2024 17:01:45 +1300 Subject: [PATCH 4/8] style: linting fix --- test-d/all-union-fields.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-d/all-union-fields.ts b/test-d/all-union-fields.ts index 8d561db10..fee71bdd1 100644 --- a/test-d/all-union-fields.ts +++ b/test-d/all-union-fields.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import {type AllUnionFields, type Simplify} from '../index'; +import type {AllUnionFields, Simplify} from '../index'; type TestingType = { function: () => void; From 0a57aa21f3ad942683e87418a0d0a6d5d81f41e1 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sat, 28 Dec 2024 12:54:43 +0530 Subject: [PATCH 5/8] doc: move types to internal and add doc --- source/all-union-fields.d.ts | 9 +-------- source/internal/object.d.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts index 9415d6b60..d366c79c7 100644 --- a/source/all-union-fields.d.ts +++ b/source/all-union-fields.d.ts @@ -1,5 +1,4 @@ -import type {NonRecursiveType} from './internal'; -import type {IsEqual} from './is-equal'; +import type {NonRecursiveType, ReadonlyKeysOfUnion, ValueOfUnion} from './internal'; import type {KeysOfUnion} from './keys-of-union'; import type {SharedUnionFields} from './shared-union-fields'; import type {Simplify} from './simplify'; @@ -78,9 +77,3 @@ export type AllUnionFields = [Union] extends [NonRecursiveType | Readonly [P in Exclude, ReadonlyKeysOfUnion | keyof Union>]?: ValueOfUnion; } >; - -type ValueOfUnion = Union extends unknown ? Key extends keyof Union ? Union[Key] : never : never; - -type ReadonlyKeysOfUnion = T extends unknown ? keyof { - [P in keyof T as IsEqual<{[Q in P]: T[P]}, {readonly [Q in P]: T[P]}> extends true ? P : never]: never -} : never; diff --git a/source/internal/object.d.ts b/source/internal/object.d.ts index bbd63a411..8f99a79d6 100644 --- a/source/internal/object.d.ts +++ b/source/internal/object.d.ts @@ -1,5 +1,6 @@ import type {Simplify} from '../simplify'; import type {UnknownArray} from '../unknown-array'; +import type {IsEqual} from '../is-equal'; import type {KeysOfUnion} from '../keys-of-union'; import type {FilterDefinedKeys, FilterOptionalKeys} from './keys'; import type {NonRecursiveType} from './type'; @@ -122,3 +123,36 @@ type IndexSignature = HomomorphicPick<{[k: string]: unknown}, number>; export type HomomorphicPick> = { [P in keyof T as Extract]: T[P] }; + +/** +Extract all possible values for a given key from a union of object types. + +@example + +type Statuses = ValueOfUnion<{ id: 1, status: "open" } | { id: 2, status: "closed" }, "status">; +//=> "open" | "closed" +*/ +export type ValueOfUnion> = + Union extends unknown ? Key extends keyof Union ? Union[Key] : never : never; + +/** +Extract all readonly keys from a union of object types. + +@example +type User = { + readonly id: string; + name: string; +}; + +type Post = { + readonly id: string; + readonly author: string; + body: string; +}; + +type ReadonlyKeys = ReadonlyKeysOfUnion; +//=> "id" | "author" +*/ +export type ReadonlyKeysOfUnion = Union extends unknown ? keyof { + [Key in keyof Union as IsEqual<{[K in Key]: Union[Key]}, {readonly [K in Key]: Union[Key]}> extends true ? Key : never]: never +} : never; From 5537126da5c05420e0aff94680997245a97c4a50 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sat, 28 Dec 2024 13:19:23 +0530 Subject: [PATCH 6/8] refactor: improve handling of recursive types --- source/all-union-fields.d.ts | 27 +++++++++++++++++---------- test-d/all-union-fields.ts | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts index d366c79c7..4e166b576 100644 --- a/source/all-union-fields.d.ts +++ b/source/all-union-fields.d.ts @@ -67,13 +67,20 @@ function displayPetInfo(petInfo: AllUnionFields) { @category Object @category Union */ -export type AllUnionFields = [Union] extends [NonRecursiveType | ReadonlyMap | ReadonlySet | UnknownArray] - ? Union - : Simplify< - SharedUnionFields & - { - readonly [P in ReadonlyKeysOfUnion]?: ValueOfUnion; - } & { - [P in Exclude, ReadonlyKeysOfUnion | keyof Union>]?: ValueOfUnion; - } - >; +export type AllUnionFields = +Extract | ReadonlySet | UnknownArray> extends infer SkippedMembers + ? Exclude extends infer RelevantMembers + ? + | SkippedMembers + | Simplify< + SharedUnionFields & + { + readonly [P in ReadonlyKeysOfUnion]?: ValueOfUnion; + } & { + [ + P in Exclude, ReadonlyKeysOfUnion | keyof RelevantMembers> + ]?: ValueOfUnion; + } + > + : never + : never; diff --git a/test-d/all-union-fields.ts b/test-d/all-union-fields.ts index fee71bdd1..3089f61c6 100644 --- a/test-d/all-union-fields.ts +++ b/test-d/all-union-fields.ts @@ -1,5 +1,6 @@ import {expectType} from 'tsd'; import type {AllUnionFields, Simplify} from '../index'; +import type {NonRecursiveType} from '../source/internal'; type TestingType = { function: () => void; @@ -167,3 +168,17 @@ expectType<{ readonly b?: string | number; readonly c?: number; }>(mixedKeywords2); + +// Non-recursive types +expectType | Map>({} as AllUnionFields | Map>); +expectType>({} as AllUnionFields>); +expectType({} as AllUnionFields); + +// Mix of non-recursive and recursive types +expectType<{a: string | number; b?: true} | undefined>({} as AllUnionFields<{a: string} | {a: number; b: true} | undefined>); +expectType({} as AllUnionFields); +expectType({} as AllUnionFields); + +// Boundary types +expectType({} as AllUnionFields); +expectType({} as AllUnionFields); From ec1dfb33f3cc4f6b91595043b4bc5b5a809373e4 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sat, 28 Dec 2024 12:56:58 +0530 Subject: [PATCH 7/8] test: add cases for value-of-union and readonly-keys-of-union --- test-d/internal/readonly-keys-of-union.ts | 27 ++++++++++++++ test-d/internal/value-of-union.ts | 44 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test-d/internal/readonly-keys-of-union.ts create mode 100644 test-d/internal/value-of-union.ts diff --git a/test-d/internal/readonly-keys-of-union.ts b/test-d/internal/readonly-keys-of-union.ts new file mode 100644 index 000000000..b3edc161e --- /dev/null +++ b/test-d/internal/readonly-keys-of-union.ts @@ -0,0 +1,27 @@ +import {expectType} from 'tsd'; +import type {ReadonlyKeysOfUnion} from '../../source/internal'; + +declare const test1: ReadonlyKeysOfUnion<{readonly a: 1; b: 2}>; +expectType<'a'>(test1); + +declare const test2: ReadonlyKeysOfUnion<{readonly a: 1; b?: 2} | {readonly c?: 3; d: 4}>; +expectType<'a' | 'c'>(test2); + +declare const test3: ReadonlyKeysOfUnion<{readonly a: 1; b?: 2} | {readonly c?: 3; d: 4} | {readonly c: 5} | {d: 6}>; +expectType<'a' | 'c'>(test3); + +// Returns `never` if there's no readonly key +declare const test4: ReadonlyKeysOfUnion<{a: 1; b?: 2} | {c?: 3; d: 4}>; +expectType(test4); + +// Works with index signatures +declare const test5: ReadonlyKeysOfUnion<{readonly [x: string]: number; a: 1} | {readonly [x: symbol]: number; a: 2}>; +expectType(test5); + +// Works with arrays +declare const test7: ReadonlyKeysOfUnion; +expectType(test7); + +// Works with functions +declare const test8: ReadonlyKeysOfUnion<(() => void) | {(): void; readonly a: 1}>; +expectType<'a'>(test8); diff --git a/test-d/internal/value-of-union.ts b/test-d/internal/value-of-union.ts new file mode 100644 index 000000000..204fc0b2b --- /dev/null +++ b/test-d/internal/value-of-union.ts @@ -0,0 +1,44 @@ +import {expectType} from 'tsd'; +import type {ValueOfUnion} from '../../source/internal'; + +// Works with objects +declare const test1: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4}, 'a'>; +expectType<1 | 3>(test1); + +// Works with arrays +declare const test2: ValueOfUnion; +expectType(test2); + +// Works with index signatures +declare const test3: ValueOfUnion<{[x: string]: string; a: 'a'} | {[x: number]: number; a: 'a'}, string>; +expectType(test3); + +declare const test4: ValueOfUnion<{[x: string]: string; a: 'a'} | {[x: number]: number; a: 'a'}, number>; +expectType(test4); + +// Works with functions +declare const test5: ValueOfUnion<(() => void) | {(): void; a: 1} | {(): void; a: 2}, 'a'>; +expectType<1 | 2>(test5); + +// Ignores objects where `Key` is missing +declare const test6: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4} | {a: 5; d: 6} | {e: 7}, 'a'>; +expectType<1 | 3 | 5>(test6); + +// Adds `undefined` when the key is optional +declare const test7: ValueOfUnion<{readonly a?: 1; b: 2} | {a: 3; c: 4}, 'a'>; +expectType<1 | 3 | undefined>(test7); + +// Works when `Key` is a union +declare const test8: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4} | {a: 5; b: 6} | {e: 7}, 'a' | 'b'>; +expectType<1 | 2 | 3 | 5 | 6>(test8); + +// @ts-expect-error - Errors if `Key` is missing from all of the objects +declare const test9: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4}, 'd'>; + +// Returns `any` when `Key` is `any` +declare const test10: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4}, any>; +expectType(test10); + +// Returns `never` when `Key` is `never` +declare const test11: ValueOfUnion<{a: 1; b: 2} | {a: 3; c: 4}, never>; +expectType(test11); From 2d1de301b500b91468af8e68d2e6ba2fe5a921e2 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sat, 28 Dec 2024 14:10:45 +0530 Subject: [PATCH 8/8] fix: satisfy constraint for value-of-union --- source/all-union-fields.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/all-union-fields.d.ts b/source/all-union-fields.d.ts index 4e166b576..7619809cb 100644 --- a/source/all-union-fields.d.ts +++ b/source/all-union-fields.d.ts @@ -75,7 +75,7 @@ Extract | ReadonlySet & { - readonly [P in ReadonlyKeysOfUnion]?: ValueOfUnion; + readonly [P in ReadonlyKeysOfUnion]?: ValueOfUnion>; } & { [ P in Exclude, ReadonlyKeysOfUnion | keyof RelevantMembers>