diff --git a/index.d.ts b/index.d.ts index 8cf8569e1..8568b462a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -25,6 +25,7 @@ export type {OmitIndexSignature} from './source/omit-index-signature'; export type {PickIndexSignature} from './source/pick-index-signature'; export type {PartialDeep, PartialDeepOptions} from './source/partial-deep'; export type {RequiredDeep} from './source/required-deep'; +export type {PickDeep} from './source/pick-deep'; export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep'; export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep'; export type {ReadonlyDeep} from './source/readonly-deep'; diff --git a/readme.md b/readme.md index 092d6e269..55e5e8684 100644 --- a/readme.md +++ b/readme.md @@ -126,6 +126,7 @@ Click the type names for complete docs. - [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys. - [`RequireOneOrNone`](source/require-one-or-none.d.ts) - Create a type that requires exactly a single key of the given keys and disallows more, or none of the given keys. - [`RequiredDeep`](source/required-deep.d.ts) - Create a deeply required version of another type. Use [`Required`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype) if you only need one level deep. +- [`PickDeep`](source/pick-deep.d.ts) - Pick properties from a deeply-nested object. Use [`Pick`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) if you only need one level deep. - [`OmitIndexSignature`](source/omit-index-signature.d.ts) - Omit any index signatures from the given object type, leaving only explicitly defined properties. - [`PickIndexSignature`](source/pick-index-signature.d.ts) - Pick only index signatures from the given object type, leaving out all explicitly defined properties. - [`PartialDeep`](source/partial-deep.d.ts) - Create a deeply optional version of another type. Use [`Partial`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) if you only need one level deep. diff --git a/source/internal.d.ts b/source/internal.d.ts index 17249dac7..b21cb367a 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -2,6 +2,7 @@ import type {Primitive} from './primitive'; import type {Simplify} from './simplify'; import type {Trim} from './trim'; import type {IsAny} from './is-any'; +import type {UnknownRecord} from './unknown-record'; // TODO: Remove for v5. export type {UnknownRecord} from './unknown-record'; @@ -24,6 +25,37 @@ export type BuildTuple; +/** +Create an object type with the given key `` and value ``. + +It will copy the prefix and optional status of the same key from the given object `CopiedFrom` into the result. + +@example +``` +type A = BuildObject<'a', string>; +//=> {a: string} + +// Copy `readonly` and `?` from the key `a` of `{readonly a?: any}` +type B = BuildObject<'a', string, {readonly a?: any}>; +//=> {readonly a?: string} +``` +*/ +export type BuildObject = + Key extends keyof CopiedFrom + ? Pick<{[_ in keyof CopiedFrom]: Value}, Key> + : Key extends `${infer NumberKey extends number}` + ? NumberKey extends keyof CopiedFrom + ? Pick<{[_ in keyof CopiedFrom]: Value}, NumberKey> + : {[_ in Key]: Value} + : {[_ in Key]: Value}; + +/** +Return a string representation of the given string or number. + +Note: This type is not the return type of the `.toString()` function. +*/ +export type ToString = T extends string | number ? `${T}` : never; + /** Create a tuple of length `A` and a tuple composed of two other tuples, the inferred tuple `U` and a tuple of length `B`, then extracts the length of tuple `U`. diff --git a/source/paths.d.ts b/source/paths.d.ts index 076372ddc..985243e46 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -1,11 +1,10 @@ +import type {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'; import type {UnknownRecord} from './unknown-record'; -type ToString = T extends string | number ? `${T}` : never; - /** Return the part of the given array with a fixed index. diff --git a/source/pick-deep.d.ts b/source/pick-deep.d.ts new file mode 100644 index 000000000..c5d9cdbf2 --- /dev/null +++ b/source/pick-deep.d.ts @@ -0,0 +1,141 @@ +import type {BuildObject, BuildTuple, ToString} from './internal'; +import type {Paths} from './paths'; +import type {Simplify} from './simplify.d'; +import type {UnionToIntersection} from './union-to-intersection.d'; +import type {UnknownArray} from './unknown-array'; +import type {UnknownRecord} from './unknown-record.d'; + +/** +Pick properties from a deeply-nested object. + +It supports recursing into arrays. + +Use-case: Distill complex objects down to the components you need to target. + +@example +``` +import type {PickDeep, PartialDeep} from 'type-fest'; + +type Configuration = { + userConfig: { + name: string; + age: number; + address: [ + { + city1: string; + street1: string; + }, + { + city2: string; + street2: string; + } + ] + }; + otherConfig: any; +}; + +type NameConfig = PickDeep; +// type NameConfig = { +// userConfig: { +// name: string; +// }; + +// Supports optional properties +type User = PickDeep, 'userConfig.name' | 'userConfig.age'>; +// type User = { +// userConfig?: { +// name?: string; +// age?: number; +// }; +// }; + +// Supports array +type AddressConfig = PickDeep; +// type AddressConfig = { +// userConfig: { +// address: [{ +// city1: string; +// street1: string; +// }]; +// }; +// } + +// Supports recurse into array +type Street = PickDeep; +// type AddressConfig = { +// userConfig: { +// address: [ +// unknown, +// {street2: string} +// ]; +// }; +// } +``` + +@category Object +@category Array +*/ +export type PickDeep> = + T extends UnknownRecord + ? Simplify; + }[PathUnion]>> + : T extends UnknownArray + ? UnionToIntersection<{ + [P in PathUnion]: InternalPickDeep; + }[PathUnion] + > + : never; + +/** +Pick an object/array from the given object/array by one path. +*/ +type InternalPickDeep< + T extends UnknownRecord | UnknownArray, + Path extends string | number, // Checked paths, extracted from unchecked paths +> = + T extends UnknownArray ? PickDeepArray + : T extends UnknownRecord ? Simplify> + : never; + +/** +Pick an object from the given object by one path. +*/ +type PickDeepObject = + P extends `${infer RecordKeyInPath}.${infer SubPath}` + ? BuildObject, SubPath>, RecordType> + : P extends keyof RecordType | ToString // Handle number keys + ? BuildObject + : never; + +/** +Pick an array from the given array by one path. +*/ +type PickDeepArray = + // Handle paths that are `${number}.${string}` + P extends `${infer ArrayIndex extends number}.${infer SubPath}` + // When `ArrayIndex` is equal to `number` + ? number extends ArrayIndex + ? ArrayType extends unknown[] + ? Array, SubPath>> + : ArrayType extends readonly unknown[] + ? ReadonlyArray, SubPath>> + : never + // When `ArrayIndex` is a number literal + : ArrayType extends unknown[] + ? [...BuildTuple, InternalPickDeep, SubPath>] + : ArrayType extends readonly unknown[] + ? readonly [...BuildTuple, InternalPickDeep, SubPath>] + : never + // When the path is equal to `number` + : P extends `${infer ArrayIndex extends number}` + // When `ArrayIndex` is `number` + ? number extends ArrayIndex + ? ArrayType + // When `ArrayIndex` is a number literal + : ArrayType extends unknown[] + ? [...BuildTuple, ArrayType[ArrayIndex]] + : ArrayType extends readonly unknown[] + ? readonly [...BuildTuple, ArrayType[ArrayIndex]] + : never + : never; diff --git a/test-d/pick-deep.ts b/test-d/pick-deep.ts new file mode 100644 index 000000000..686fe24b2 --- /dev/null +++ b/test-d/pick-deep.ts @@ -0,0 +1,112 @@ +import {expectType} from 'tsd'; +import type {PickDeep} from '../index'; + +declare class ClassA { + a: string; +} + +type BaseType = { + string: string; + optionalString?: string; + array: number[]; + readonlyArray: readonly number[]; + tuples: ['foo', 'bar']; + objectArray: Array<{a: 1; b: 2}>; + leadingSpreadArray: [...Array<{a: 1}>, {b: 2}]; + tailingSpreadArray: [{a: 1}, {b: {c: 2; other: 2}}, ...Array<{d: 3}>]; + objectTuple: [{a: 1}]; + number: number; + boolean: boolean; + date: Date; + Class: typeof ClassA; + instance: ClassA; + 0: number; +}; + +type Testing = BaseType & { + object: BaseType; + optionalObject?: Partial; + optionalString?: string; + readonly readonlyObject: {a: 1}; + 1: BaseType; + 2?: BaseType; +}; + +declare const normal: PickDeep; +expectType<{string: string}>(normal); + +type DeepType = { + nested: { + deep: { + deeper: { + value: string; + }; + }; + }; + foo: string; +}; +declare const deep: PickDeep; +expectType<{nested: {deep: {deeper: {value: string}}}}>(deep); + +type GenericType = { + genericKey: T; +}; +declare const genericTest: PickDeep, 'genericKey'>; +expectType<{genericKey: number}>(genericTest); + +declare const union: PickDeep; +expectType<{object: {number: number} & {string: string}}>(union); + +declare const optional: PickDeep; +expectType<{optionalObject?: {optionalString?: string}}>(optional); + +declare const optionalUnion: PickDeep; +expectType<{optionalObject?: {string?: string}; object: {number: number}}>(optionalUnion); + +declare const readonlyTest: PickDeep; +expectType<{readonly readonlyObject: {a: 1}}>(readonlyTest); + +declare const array: PickDeep; +expectType<{object: {array: number[]}}>(array); + +declare const readonlyArray: PickDeep; +expectType<{object: {readonlyArray: readonly number[]}}>(readonlyArray); + +declare const tuple: PickDeep; +expectType<{object: {tuples: ['foo', 'bar']}}>(tuple); + +declare const objectArray1: PickDeep; +expectType<{object: {objectArray: Array<{a: 1; b: 2}>}}>(objectArray1); + +declare const objectArray2: PickDeep; +expectType<{object: {objectArray: Array<{a: 1}>}}>(objectArray2); + +declare const leadingSpreadArray1: PickDeep; +expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>]}}>(leadingSpreadArray1); + +declare const leadingSpreadArray2: PickDeep; +expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>, {b: 2}]}}>(leadingSpreadArray2); + +declare const tailingSpreadArray1: PickDeep; +expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2; other: 2}}]}}>(tailingSpreadArray1); + +declare const tailingSpreadArray2: PickDeep; +expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2}}]}}>(tailingSpreadArray2); + +declare const date: PickDeep; +expectType<{object: {date: Date}}>(date); + +declare const instance: PickDeep; +expectType<{object: {instance: ClassA}}>(instance); + +declare const classTest: PickDeep; +expectType<{object: {Class: typeof ClassA}}>(classTest); + +declare const numberTest: PickDeep; +expectType<{1: BaseType}>(numberTest); + +declare const numberTest2: PickDeep; +expectType<{1: {0: number}}>(numberTest2); + +declare const numberTest3: PickDeep; +expectType<{2?: {0: number}}>(numberTest3);