From 94bb3d3fd739588860a2a0a0021a4dca7abbe578 Mon Sep 17 00:00:00 2001 From: Haozheng Li Date: Thu, 28 Dec 2023 01:05:40 +0800 Subject: [PATCH] Add `SharedUnionFieldsDeep` type (#783) Co-authored-by: Sindre Sorhus --- index.d.ts | 1 + readme.md | 1 + source/internal.d.ts | 112 +++++++++++++++- source/paths.d.ts | 38 +----- source/shared-union-fields-deep.d.ts | 192 +++++++++++++++++++++++++++ test-d/shared-union-fields-deep.ts | 125 +++++++++++++++++ 6 files changed, 432 insertions(+), 37 deletions(-) create mode 100644 source/shared-union-fields-deep.d.ts create mode 100644 test-d/shared-union-fields-deep.ts diff --git a/index.d.ts b/index.d.ts index 8568b462a..d86f5b4a6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -105,6 +105,7 @@ export type {ArrayIndices} from './source/array-indices'; export type {ArrayValues} from './source/array-values'; export type {SetFieldType} from './source/set-field-type'; export type {Paths} from './source/paths'; +export type {SharedUnionFieldsDeep} from './source/shared-union-fields-deep'; // Template literal types export type {CamelCase} from './source/camel-case'; diff --git a/readme.md b/readme.md index 55e5e8684..b4f5762d9 100644 --- a/readme.md +++ b/readme.md @@ -179,6 +179,7 @@ Click the type names for complete docs. - [`ArrayValues`](source/array-values.d.ts) - Provides all values for a constant array or tuple. - [`SetFieldType`](source/set-field-type.d.ts) - Create a type that changes the type of the given keys. - [`Paths`](source/paths.d.ts) - Generate a union of all possible paths to properties in the given object. +- [`SharedUnionFieldsDeep`](source/shared-union-fields-deep.d.ts) - Create a type with shared fields from a union of object types, deeply traversing nested structures. ### Type Guard diff --git a/source/internal.d.ts b/source/internal.d.ts index 00f0823c4..e9ea5fa81 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -3,6 +3,7 @@ import type {Simplify} from './simplify'; import type {Trim} from './trim'; import type {IsAny} from './is-any'; import type {UnknownRecord} from './unknown-record'; +import type {IsNever} from './is-never'; import type {UnknownArray} from './unknown-array'; // TODO: Remove for v5. @@ -13,7 +14,34 @@ Infer the length of the given array ``. @link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f */ -type TupleLength = T extends {readonly length: infer L} ? L : never; +type ArrayLength = T extends {readonly length: infer L} ? L : never; + +/** +Infer the length of the given tuple ``. + +Returns `never` if the given type is an non-fixed-length array like `Array`. + +@example +``` +type Tuple = TupleLength<[string, number, boolean]>; +//=> 3 + +type Array = TupleLength; +//=> never + +// Supports union types. +type Union = TupleLength<[] | [1, 2, 3] | Array>; +//=> 1 | 3 +``` +*/ +export type TupleLength = + // `extends unknown` is used to convert `T` (if `T` is a union type) to + // a [distributive conditionaltype](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types)) + T extends unknown + ? number extends T['length'] + ? never // Return never if the given type is an non-flexed-length array like `Array` + : T['length'] + : never; // Should never happen /** Create a tuple type of the given length `` and fill it with the given type ``. @@ -64,7 +92,7 @@ the inferred tuple `U` and a tuple of length `B`, then extracts the length of tu @link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f */ export type Subtract = BuildTuple extends [...(infer U), ...BuildTuple] - ? TupleLength + ? ArrayLength : never; /** @@ -328,3 +356,83 @@ IsPrimitive ``` */ export type IsPrimitive = [T] extends [Primitive] ? true : false; + +/** +Returns the static, fixed-length portion of the given array, excluding variable-length parts. + +@example +``` +type A = [string, number, boolean, ...string[]]; +type B = StaticPartOfArray; +//=> [string, number, boolean] +``` +*/ +export type StaticPartOfArray = + T extends unknown + ? number extends T['length'] ? + T extends readonly [infer U, ...infer V] + ? StaticPartOfArray + : Result + : T + : never; // Should never happen + +/** +Returns the variable, non-fixed-length portion of the given array, excluding static-length parts. + +@example +``` +type A = [string, number, boolean, ...string[]]; +type B = VariablePartOfArray; +//=> string[] +``` +*/ +export type VariablePartOfArray = + T extends unknown + ? T extends readonly [...StaticPartOfArray, ...infer U] + ? U + : [] + : never; // Should never happen + +/** +Returns the minimum number in the given union of numbers. + +Note: Just supports numbers from 0 to 999. + +@example +``` +type A = UnionMin<3 | 1 | 2>; +//=> 1 +``` +*/ +export type UnionMin = InternalUnionMin; + +/** +The actual implementation of `UnionMin`. It's private because it has some arguments that don't need to be exposed. +*/ +type InternalUnionMin = + T['length'] extends N + ? T['length'] + : InternalUnionMin; + +/** +Returns the maximum number in the given union of numbers. + +Note: Just supports numbers from 0 to 999. + +@example +``` +type A = UnionMax<1 | 3 | 2>; +//=> 3 +``` +*/ +export type UnionMax = InternalUnionMax; + +/** +The actual implementation of `UnionMax`. It's private because it has some arguments that don't need to be exposed. +*/ +type InternalUnionMax = + IsNever extends true + ? T['length'] + : T['length'] extends N + ? InternalUnionMax, T> + : InternalUnionMax; diff --git a/source/paths.d.ts b/source/paths.d.ts index 1ace55055..f7d18214c 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -1,41 +1,9 @@ -import type {NonRecursiveType, ToString} from './internal'; +import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString} from './internal'; import type {EmptyObject} from './empty-object'; import type {IsAny} from './is-any'; import type {IsNever} from './is-never'; import type {UnknownArray} from './unknown-array'; -/** -Return the part of the given array with a fixed index. - -@example -``` -type A = [string, number, boolean, ...string[]]; -type B = FilterFixedIndexArray; -//=> [string, number, boolean] -``` -*/ -type FilterFixedIndexArray = - number extends T['length'] ? - T extends readonly [infer U, ...infer V] - ? FilterFixedIndexArray - : Result - : T; - -/** -Return the part of the given array with a non-fixed index. - -@example -``` -type A = [string, number, boolean, ...string[]]; -type B = FilterNotFixedIndexArray; -//=> string[] -``` -*/ -type FilterNotFixedIndexArray = -T extends readonly [...FilterFixedIndexArray, ...infer U] - ? U - : []; - /** Generate a union of all possible paths to properties in the given object. @@ -85,8 +53,8 @@ export type Paths = : T extends UnknownArray ? number extends T['length'] // We need to handle the fixed and non-fixed index part of the array separately. - ? InternalPaths> - | InternalPaths[number]>> + ? InternalPaths> + | InternalPaths[number]>> : InternalPaths : T extends object ? InternalPaths diff --git a/source/shared-union-fields-deep.d.ts b/source/shared-union-fields-deep.d.ts new file mode 100644 index 000000000..f4dbb8b69 --- /dev/null +++ b/source/shared-union-fields-deep.d.ts @@ -0,0 +1,192 @@ +import type {NonRecursiveType, UnionMin, UnionMax, TupleLength, StaticPartOfArray, VariablePartOfArray} from './internal'; +import type {IsNever} from './is-never'; +import type {UnknownArray} from './unknown-array'; + +/** +Set the given array to readonly if `IsReadonly` is `true`, otherwise set the given array to normal, then return the result. + +@example +``` +type ReadonlyArray = readonly string[]; +type NormalArray = string[]; + +type ReadonlyResult = SetArrayAccess; +//=> readonly string[] + +type NormalResult = SetArrayAccess; +//=> string[] +``` +*/ +type SetArrayAccess = + T extends readonly [...infer U] ? + IsReadonly extends true + ? readonly [...U] + : [...U] + : T; + +/** +Returns whether the given array `T` is readonly. +*/ +type IsArrayReadonly = T extends unknown[] ? false : true; + +/** +SharedUnionFieldsDeep options. + +@see {@link SharedUnionFieldsDeep} +*/ +export type SharedUnionFieldsDeepOptions = { + /** + When set to true, this option impacts each element within arrays or tuples. If all union values are arrays or tuples, it constructs an array of the shortest possible length, ensuring every element exists in the union array. + + @default false + */ + recurseIntoArrays?: boolean; +}; + +/** +Create a type with shared fields from a union of object types, deeply traversing nested structures. + +Use the {@link SharedUnionFieldsDeepOptions `Options`} to specify the behavior for arrays. + +Use-cases: +- You want a safe object type where each key exists in the union object. +- You want to focus on the common fields of the union type and don't want to have to care about the other fields. + +@example +``` +import type {SharedUnionFieldsDeep} from 'type-fest'; + +type Cat = { + info: { + name: string; + type: 'cat'; + catType: string; + }; +}; + +type Dog = { + info: { + name: string; + type: 'dog'; + dogType: string; + }; +}; + +function displayPetInfo(petInfo: (Cat | Dog)['info']) { + // typeof petInfo => + // { + // name: string; + // type: 'cat'; + // catType: string; // Needn't care about this field, because it's not a common pet info field. + // } | { + // name: string; + // type: 'dog'; + // dogType: string; // Needn't care about this field, because it's not a common pet info field. + // } + + // petInfo type is complex and have some needless fields + + console.log('name: ', petInfo.name); + console.log('type: ', petInfo.type); +} + +function displayPetInfo(petInfo: SharedUnionFieldsDeep['info']) { + // typeof petInfo => + // { + // name: string; + // type: 'cat' | 'dog'; + // } + + // petInfo type is simple and clear + + console.log('name: ', petInfo.name); + console.log('type: ', petInfo.type); +} +``` + +@category Object +@category Union +*/ +export type SharedUnionFieldsDeep = + // `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] + ? Union + : [Union] extends [UnknownArray] + ? Options['recurseIntoArrays'] extends true + ? SetArrayAccess, IsArrayReadonly> + : Union + : [Union] extends [object] + ? SharedObjectUnionFieldsDeep + : Union; + +/** +Same as `SharedUnionFieldsDeep`, but accepts only `object`s and as inputs. Internal helper for `SharedUnionFieldsDeep`. +*/ +type SharedObjectUnionFieldsDeep = + keyof Union extends infer Keys + ? IsNever extends false + ? { + [Key in keyof Union]: + Union[Key] extends NonRecursiveType + ? Union[Key] + : SharedUnionFieldsDeep + } + : {} + : Union; + +/** +Same as `SharedUnionFieldsDeep`, but accepts only `UnknownArray`s and as inputs. Internal helper for `SharedUnionFieldsDeep`. +*/ +type SharedArrayUnionFieldsDeep = + // Restore the readonly modifier of the array. + SetArrayAccess< + InternalSharedArrayUnionFieldsDeep, + IsArrayReadonly + >; + +/** +Internal helper for `SharedArrayUnionFieldsDeep`. Needn't care the `readonly` modifier of arrays. +*/ +type InternalSharedArrayUnionFieldsDeep< + Union extends UnknownArray, + Options extends SharedUnionFieldsDeepOptions, + ResultTuple extends UnknownArray = [], +> = + // We should build a minimum possible length tuple where each element in the tuple exists in the union tuple. + IsNever> extends true + // Rule 1: If all the arrays in the union have non-fixed lengths, + // like `Array | [number, ...string[]]` + // we should build a tuple that is [the_fixed_parts_of_union, ...the_rest_of_union[]]. + // For example: `InternalSharedArrayUnionFieldsDeep | [number, ...string[]]>` + // => `[string | number, ...string[]]`. + ? ResultTuple['length'] extends UnionMax['length']> + ? [ + // The fixed-length part of the tuple. + ...ResultTuple, + // The rest of the union. + // Due to `ResultTuple` is the maximum possible fixed-length part of the tuple, + // so we can use `StaticPartOfArray` to get the rest of the union. + ...Array< + SharedUnionFieldsDeep[number], Options> + >, + ] + // Build the fixed-length tuple recursively. + : InternalSharedArrayUnionFieldsDeep< + Union, Options, + [...ResultTuple, SharedUnionFieldsDeep] + > + // Rule 2: If at least one of the arrays in the union have fixed lengths, + // like `Array | [number, string]`, + // we should build a tuple of the smallest possible length to ensure any + // item in the result tuple exists in the union tuple. + // For example: `InternalSharedArrayUnionFieldsDeep | [number, string]>` + // => `[string | number, string]`. + : ResultTuple['length'] extends UnionMin> + ? ResultTuple + // As above, build tuple recursively. + : InternalSharedArrayUnionFieldsDeep< + Union, Options, + [...ResultTuple, SharedUnionFieldsDeep] + >; diff --git a/test-d/shared-union-fields-deep.ts b/test-d/shared-union-fields-deep.ts new file mode 100644 index 000000000..005f2f409 --- /dev/null +++ b/test-d/shared-union-fields-deep.ts @@ -0,0 +1,125 @@ +import {expectType} from 'tsd'; +import type {SharedUnionFieldsDeep} from '../index'; + +type TestingType = { + function: (() => void); + record: Record; + 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}>; + array: Array<{a: number; b: string}>; + readonlyArray: ReadonlyArray<{a: number; b: string}>; +}; + +type SharedUnionFieldsDeepRecurseIntoArrays = SharedUnionFieldsDeep; + +declare const normal: SharedUnionFieldsDeep; +expectType<{string: string; number: number}>(normal); + +declare const normal2: SharedUnionFieldsDeep; +expectType<{string: string}>(normal2); + +declare const unMatched: SharedUnionFieldsDeep; +expectType<{}>(unMatched); + +declare const number: SharedUnionFieldsDeep; +expectType<{number: number}>(number); + +declare const string: SharedUnionFieldsDeep; +expectType<{string: string}>(string); + +declare const boolean: SharedUnionFieldsDeep; +expectType<{boolean: boolean}>(boolean); + +declare const date: SharedUnionFieldsDeep; +expectType<{date: Date}>(date); + +declare const regexp: SharedUnionFieldsDeep; +expectType<{regexp: RegExp}>(regexp); + +declare const symbol: SharedUnionFieldsDeep; +expectType<{symbol: symbol}>(symbol); + +declare const null_: SharedUnionFieldsDeep; +expectType<{null: null}>(null_); + +declare const undefined_: SharedUnionFieldsDeep; +expectType<{undefined: undefined}>(undefined_); + +declare const optional: SharedUnionFieldsDeep; +expectType<{optional?: boolean | string | undefined}>(optional); + +declare const propertyWithKeyword: SharedUnionFieldsDeep; +expectType<{readonly propertyWithKeyword: boolean | string}>(propertyWithKeyword); + +declare const map: SharedUnionFieldsDeep; foo: any}>; +expectType<{map: TestingType['map'] | Map}>(map); + +declare const set: SharedUnionFieldsDeep; foo: any}>; +expectType<{set: TestingType['set'] | Set}>(set); + +declare const union: SharedUnionFieldsDeep; +expectType<{string: string}>(union); + +declare const union2: SharedUnionFieldsDeep; +expectType<{union: 'test1' | 'test2' | {a: number}}>(union2); + +/** Test for array */ +declare const array: SharedUnionFieldsDeepRecurseIntoArrays; foo: any}>; +expectType<{array: Array<{a: number}>}>(array); + +declare const arrayWithoutRecursive: SharedUnionFieldsDeep; foo: any}>; +expectType<{array: TestingType['array'] | Array<{a: number; bar: string}>}>(arrayWithoutRecursive); + +declare const readonlyArray: SharedUnionFieldsDeepRecurseIntoArrays; foo: any}>; +expectType<{readonlyArray: ReadonlyArray<{a: number}>}>(readonlyArray); + +/** Test for tuple */ +declare const tuple: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number]} | {tuple: [number, string]; foo: any}>; +expectType<{tuple: [number]}>(tuple); + +declare const tupleWithoutRecursive: SharedUnionFieldsDeep<{tuple: [number]} | {tuple: [number, string]; foo: any}>; +expectType<{tuple: [number] | [number, string]}>(tupleWithoutRecursive); + +declare const tupleOrArray: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: boolean[]} | {tuple: [number, string]}>; +expectType<{tuple: [number | boolean, string | boolean]}>(tupleOrArray); + +/** Test for fixed length tuple */ +declare const fixedLengthTuple: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, string]} | {tuple: [number, string, boolean]}>; +expectType<{tuple: [number, string]}>(fixedLengthTuple); + +declare const fixedLengthTuple2: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, string]} | {tuple: []}>; +expectType<{tuple: []}>(fixedLengthTuple2); + +declare const fixedLengthTuple3: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, string, number]} | {tuple: [number, boolean, ...string[]]}>; +expectType<{tuple: [number, string | boolean, number | string]}>(fixedLengthTuple3); + +declare const threeLengthTuple: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, string]} | {tuple: [number, string, boolean]} | {tuple: number[]}>; +expectType<{tuple: [number, string | number]}>(threeLengthTuple); + +/** Test for non-fixed length tuple */ +declare const nonFixedLengthTuple: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, ...string[]]} | {tuple: boolean[]}>; +expectType<{tuple: [number | boolean, ...Array]}>(nonFixedLengthTuple); + +declare const nonFixedLengthTuple2: SharedUnionFieldsDeepRecurseIntoArrays<{tuple: [number, ...string[]]} | {tuple: [number, string, ...boolean[]]}>; +expectType<{tuple: [number, string, ...Array]}>(nonFixedLengthTuple2);