Skip to content

Commit c60caba

Browse files
Emiyaaaaasindresorhus
andauthoredNov 8, 2023
Add PickDeep type (#737)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 86ddc1a commit c60caba

File tree

6 files changed

+288
-2
lines changed

6 files changed

+288
-2
lines changed
 

‎index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type {OmitIndexSignature} from './source/omit-index-signature';
2525
export type {PickIndexSignature} from './source/pick-index-signature';
2626
export type {PartialDeep, PartialDeepOptions} from './source/partial-deep';
2727
export type {RequiredDeep} from './source/required-deep';
28+
export type {PickDeep} from './source/pick-deep';
2829
export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep';
2930
export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep';
3031
export type {ReadonlyDeep} from './source/readonly-deep';

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Click the type names for complete docs.
126126
- [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys.
127127
- [`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.
128128
- [`RequiredDeep`](source/required-deep.d.ts) - Create a deeply required version of another type. Use [`Required<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype) if you only need one level deep.
129+
- [`PickDeep`](source/pick-deep.d.ts) - Pick properties from a deeply-nested object. Use [`Pick<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) if you only need one level deep.
129130
- [`OmitIndexSignature`](source/omit-index-signature.d.ts) - Omit any index signatures from the given object type, leaving only explicitly defined properties.
130131
- [`PickIndexSignature`](source/pick-index-signature.d.ts) - Pick only index signatures from the given object type, leaving out all explicitly defined properties.
131132
- [`PartialDeep`](source/partial-deep.d.ts) - Create a deeply optional version of another type. Use [`Partial<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) if you only need one level deep.

‎source/internal.d.ts

+32
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {Primitive} from './primitive';
22
import type {Simplify} from './simplify';
33
import type {Trim} from './trim';
44
import type {IsAny} from './is-any';
5+
import type {UnknownRecord} from './unknown-record';
56

67
// TODO: Remove for v5.
78
export type {UnknownRecord} from './unknown-record';
@@ -24,6 +25,37 @@ export type BuildTuple<L extends number, Fill = unknown, T extends readonly unkn
2425
? T
2526
: BuildTuple<L, Fill, [...T, Fill]>;
2627

28+
/**
29+
Create an object type with the given key `<Key>` and value `<Value>`.
30+
31+
It will copy the prefix and optional status of the same key from the given object `CopiedFrom` into the result.
32+
33+
@example
34+
```
35+
type A = BuildObject<'a', string>;
36+
//=> {a: string}
37+
38+
// Copy `readonly` and `?` from the key `a` of `{readonly a?: any}`
39+
type B = BuildObject<'a', string, {readonly a?: any}>;
40+
//=> {readonly a?: string}
41+
```
42+
*/
43+
export type BuildObject<Key extends PropertyKey, Value, CopiedFrom extends UnknownRecord = {}> =
44+
Key extends keyof CopiedFrom
45+
? Pick<{[_ in keyof CopiedFrom]: Value}, Key>
46+
: Key extends `${infer NumberKey extends number}`
47+
? NumberKey extends keyof CopiedFrom
48+
? Pick<{[_ in keyof CopiedFrom]: Value}, NumberKey>
49+
: {[_ in Key]: Value}
50+
: {[_ in Key]: Value};
51+
52+
/**
53+
Return a string representation of the given string or number.
54+
55+
Note: This type is not the return type of the `.toString()` function.
56+
*/
57+
export type ToString<T> = T extends string | number ? `${T}` : never;
58+
2759
/**
2860
Create a tuple of length `A` and a tuple composed of two other tuples,
2961
the inferred tuple `U` and a tuple of length `B`, then extracts the length of tuple `U`.

‎source/paths.d.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
import type {ToString} from './internal';
12
import type {EmptyObject} from './empty-object';
23
import type {IsAny} from './is-any';
34
import type {IsNever} from './is-never';
45
import type {UnknownArray} from './unknown-array';
56
import type {UnknownRecord} from './unknown-record';
67

7-
type ToString<T> = T extends string | number ? `${T}` : never;
8-
98
/**
109
Return the part of the given array with a fixed index.
1110

‎source/pick-deep.d.ts

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type {BuildObject, BuildTuple, ToString} from './internal';
2+
import type {Paths} from './paths';
3+
import type {Simplify} from './simplify.d';
4+
import type {UnionToIntersection} from './union-to-intersection.d';
5+
import type {UnknownArray} from './unknown-array';
6+
import type {UnknownRecord} from './unknown-record.d';
7+
8+
/**
9+
Pick properties from a deeply-nested object.
10+
11+
It supports recursing into arrays.
12+
13+
Use-case: Distill complex objects down to the components you need to target.
14+
15+
@example
16+
```
17+
import type {PickDeep, PartialDeep} from 'type-fest';
18+
19+
type Configuration = {
20+
userConfig: {
21+
name: string;
22+
age: number;
23+
address: [
24+
{
25+
city1: string;
26+
street1: string;
27+
},
28+
{
29+
city2: string;
30+
street2: string;
31+
}
32+
]
33+
};
34+
otherConfig: any;
35+
};
36+
37+
type NameConfig = PickDeep<Configuration, 'userConfig.name'>;
38+
// type NameConfig = {
39+
// userConfig: {
40+
// name: string;
41+
// };
42+
43+
// Supports optional properties
44+
type User = PickDeep<PartialDeep<Configuration>, 'userConfig.name' | 'userConfig.age'>;
45+
// type User = {
46+
// userConfig?: {
47+
// name?: string;
48+
// age?: number;
49+
// };
50+
// };
51+
52+
// Supports array
53+
type AddressConfig = PickDeep<Configuration, `userConfig.address.0`>;
54+
// type AddressConfig = {
55+
// userConfig: {
56+
// address: [{
57+
// city1: string;
58+
// street1: string;
59+
// }];
60+
// };
61+
// }
62+
63+
// Supports recurse into array
64+
type Street = PickDeep<Configuration, `userConfig.address.1.street2`>;
65+
// type AddressConfig = {
66+
// userConfig: {
67+
// address: [
68+
// unknown,
69+
// {street2: string}
70+
// ];
71+
// };
72+
// }
73+
```
74+
75+
@category Object
76+
@category Array
77+
*/
78+
export type PickDeep<T extends UnknownRecord | UnknownArray, PathUnion extends Paths<T>> =
79+
T extends UnknownRecord
80+
? Simplify<UnionToIntersection<{
81+
[P in PathUnion]: InternalPickDeep<T, P>;
82+
}[PathUnion]>>
83+
: T extends UnknownArray
84+
? UnionToIntersection<{
85+
[P in PathUnion]: InternalPickDeep<T, P>;
86+
}[PathUnion]
87+
>
88+
: never;
89+
90+
/**
91+
Pick an object/array from the given object/array by one path.
92+
*/
93+
type InternalPickDeep<
94+
T extends UnknownRecord | UnknownArray,
95+
Path extends string | number, // Checked paths, extracted from unchecked paths
96+
> =
97+
T extends UnknownArray ? PickDeepArray<T, Path>
98+
: T extends UnknownRecord ? Simplify<PickDeepObject<T, Path>>
99+
: never;
100+
101+
/**
102+
Pick an object from the given object by one path.
103+
*/
104+
type PickDeepObject<RecordType extends UnknownRecord, P extends string | number> =
105+
P extends `${infer RecordKeyInPath}.${infer SubPath}`
106+
? BuildObject<RecordKeyInPath, InternalPickDeep<NonNullable<RecordType[RecordKeyInPath]>, SubPath>, RecordType>
107+
: P extends keyof RecordType | ToString<keyof RecordType> // Handle number keys
108+
? BuildObject<P, RecordType[P], RecordType>
109+
: never;
110+
111+
/**
112+
Pick an array from the given array by one path.
113+
*/
114+
type PickDeepArray<ArrayType extends UnknownArray, P extends string | number> =
115+
// Handle paths that are `${number}.${string}`
116+
P extends `${infer ArrayIndex extends number}.${infer SubPath}`
117+
// When `ArrayIndex` is equal to `number`
118+
? number extends ArrayIndex
119+
? ArrayType extends unknown[]
120+
? Array<InternalPickDeep<NonNullable<ArrayType[number]>, SubPath>>
121+
: ArrayType extends readonly unknown[]
122+
? ReadonlyArray<InternalPickDeep<NonNullable<ArrayType[number]>, SubPath>>
123+
: never
124+
// When `ArrayIndex` is a number literal
125+
: ArrayType extends unknown[]
126+
? [...BuildTuple<ArrayIndex>, InternalPickDeep<NonNullable<ArrayType[ArrayIndex]>, SubPath>]
127+
: ArrayType extends readonly unknown[]
128+
? readonly [...BuildTuple<ArrayIndex>, InternalPickDeep<NonNullable<ArrayType[ArrayIndex]>, SubPath>]
129+
: never
130+
// When the path is equal to `number`
131+
: P extends `${infer ArrayIndex extends number}`
132+
// When `ArrayIndex` is `number`
133+
? number extends ArrayIndex
134+
? ArrayType
135+
// When `ArrayIndex` is a number literal
136+
: ArrayType extends unknown[]
137+
? [...BuildTuple<ArrayIndex>, ArrayType[ArrayIndex]]
138+
: ArrayType extends readonly unknown[]
139+
? readonly [...BuildTuple<ArrayIndex>, ArrayType[ArrayIndex]]
140+
: never
141+
: never;

‎test-d/pick-deep.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {expectType} from 'tsd';
2+
import type {PickDeep} from '../index';
3+
4+
declare class ClassA {
5+
a: string;
6+
}
7+
8+
type BaseType = {
9+
string: string;
10+
optionalString?: string;
11+
array: number[];
12+
readonlyArray: readonly number[];
13+
tuples: ['foo', 'bar'];
14+
objectArray: Array<{a: 1; b: 2}>;
15+
leadingSpreadArray: [...Array<{a: 1}>, {b: 2}];
16+
tailingSpreadArray: [{a: 1}, {b: {c: 2; other: 2}}, ...Array<{d: 3}>];
17+
objectTuple: [{a: 1}];
18+
number: number;
19+
boolean: boolean;
20+
date: Date;
21+
Class: typeof ClassA;
22+
instance: ClassA;
23+
0: number;
24+
};
25+
26+
type Testing = BaseType & {
27+
object: BaseType;
28+
optionalObject?: Partial<BaseType>;
29+
optionalString?: string;
30+
readonly readonlyObject: {a: 1};
31+
1: BaseType;
32+
2?: BaseType;
33+
};
34+
35+
declare const normal: PickDeep<Testing, 'string'>;
36+
expectType<{string: string}>(normal);
37+
38+
type DeepType = {
39+
nested: {
40+
deep: {
41+
deeper: {
42+
value: string;
43+
};
44+
};
45+
};
46+
foo: string;
47+
};
48+
declare const deep: PickDeep<DeepType, 'nested.deep.deeper.value'>;
49+
expectType<{nested: {deep: {deeper: {value: string}}}}>(deep);
50+
51+
type GenericType<T> = {
52+
genericKey: T;
53+
};
54+
declare const genericTest: PickDeep<GenericType<number>, 'genericKey'>;
55+
expectType<{genericKey: number}>(genericTest);
56+
57+
declare const union: PickDeep<Testing, 'object.number' | 'object.string'>;
58+
expectType<{object: {number: number} & {string: string}}>(union);
59+
60+
declare const optional: PickDeep<Testing, 'optionalObject.optionalString'>;
61+
expectType<{optionalObject?: {optionalString?: string}}>(optional);
62+
63+
declare const optionalUnion: PickDeep<Testing, 'optionalObject.string' | 'object.number'>;
64+
expectType<{optionalObject?: {string?: string}; object: {number: number}}>(optionalUnion);
65+
66+
declare const readonlyTest: PickDeep<Testing, 'readonlyObject.a'>;
67+
expectType<{readonly readonlyObject: {a: 1}}>(readonlyTest);
68+
69+
declare const array: PickDeep<Testing, 'object.array'>;
70+
expectType<{object: {array: number[]}}>(array);
71+
72+
declare const readonlyArray: PickDeep<Testing, 'object.readonlyArray'>;
73+
expectType<{object: {readonlyArray: readonly number[]}}>(readonlyArray);
74+
75+
declare const tuple: PickDeep<Testing, 'object.tuples'>;
76+
expectType<{object: {tuples: ['foo', 'bar']}}>(tuple);
77+
78+
declare const objectArray1: PickDeep<Testing, `object.objectArray.${number}`>;
79+
expectType<{object: {objectArray: Array<{a: 1; b: 2}>}}>(objectArray1);
80+
81+
declare const objectArray2: PickDeep<Testing, `object.objectArray.${number}.a`>;
82+
expectType<{object: {objectArray: Array<{a: 1}>}}>(objectArray2);
83+
84+
declare const leadingSpreadArray1: PickDeep<Testing, `object.leadingSpreadArray.${number}.a`>;
85+
expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>]}}>(leadingSpreadArray1);
86+
87+
declare const leadingSpreadArray2: PickDeep<Testing, `object.leadingSpreadArray.${number}`>;
88+
expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>, {b: 2}]}}>(leadingSpreadArray2);
89+
90+
declare const tailingSpreadArray1: PickDeep<Testing, 'object.tailingSpreadArray.1'>;
91+
expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2; other: 2}}]}}>(tailingSpreadArray1);
92+
93+
declare const tailingSpreadArray2: PickDeep<Testing, 'object.tailingSpreadArray.1.b.c'>;
94+
expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2}}]}}>(tailingSpreadArray2);
95+
96+
declare const date: PickDeep<Testing, 'object.date'>;
97+
expectType<{object: {date: Date}}>(date);
98+
99+
declare const instance: PickDeep<Testing, 'object.instance'>;
100+
expectType<{object: {instance: ClassA}}>(instance);
101+
102+
declare const classTest: PickDeep<Testing, 'object.Class'>;
103+
expectType<{object: {Class: typeof ClassA}}>(classTest);
104+
105+
declare const numberTest: PickDeep<Testing, '1'>;
106+
expectType<{1: BaseType}>(numberTest);
107+
108+
declare const numberTest2: PickDeep<Testing, '1.0'>;
109+
expectType<{1: {0: number}}>(numberTest2);
110+
111+
declare const numberTest3: PickDeep<Testing, '2.0'>;
112+
expectType<{2?: {0: number}}>(numberTest3);

0 commit comments

Comments
 (0)
Please sign in to comment.