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..7619809cb --- /dev/null +++ b/source/all-union-fields.d.ts @@ -0,0 +1,86 @@ +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'; +import type {UnknownArray} from './unknown-array'; + +/** +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?: string; + // dogType?: string; + // } + + 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 = +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/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; 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..3089f61c6 --- /dev/null +++ b/test-d/all-union-fields.ts @@ -0,0 +1,184 @@ +import {expectType} from 'tsd'; +import type {AllUnionFields, Simplify} from '../index'; +import type {NonRecursiveType} from '../source/internal'; + +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}>; +}; + +declare const normal: AllUnionFields< +TestingType | {string: string; number: number; foo: any} +>; +expectType> +>>(normal); + +declare const unMatched: AllUnionFields; +expectType +>>(unMatched); + +declare const number: AllUnionFields; +expectType> +>>(number); + +declare const string: AllUnionFields; +expectType> +>>(string); + +declare const boolean: AllUnionFields; +expectType> +>>(boolean); + +declare const date: AllUnionFields; +expectType> +>>(date); + +declare const regexp: AllUnionFields; +expectType> +>>(regexp); + +declare const symbol: AllUnionFields; +expectType> +>>(symbol); + +declare const null_: AllUnionFields; +expectType> +>>(null_); + +declare const undefined_: AllUnionFields; +expectType> +>>(undefined_); + +declare const optional: AllUnionFields; +expectType> +>>(optional); + +declare const propertyWithKeyword: AllUnionFields; +expectType> +>>(propertyWithKeyword); + +declare const map: AllUnionFields; foo: any}>; +expectType; + foo?: any; +} & Partial> +>>(map); + +declare const set: AllUnionFields; foo: any}>; +expectType; + foo?: any; +} & Partial> +>>(set); + +declare const moreUnion: AllUnionFields; +expectType> +>>(moreUnion); + +declare const union: AllUnionFields; +expectType> +>>(union); + +declare const unionWithOptional: AllUnionFields<{a?: string; foo: number} | {a: string; bar: string}>; +expectType<{ + a?: string; + foo?: number; + bar?: string; +}>(unionWithOptional); + +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 mixedKeywords2: AllUnionFields<{readonly a: string; b: number} | {a: string; readonly b: string} | {readonly c: number}>; +expectType<{ + readonly a?: string; + 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); 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);