Skip to content

Commit

Permalink
Add PickDeep type (#737)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Emiyaaaaa and sindresorhus committed Nov 8, 2023
1 parent 86ddc1a commit c60caba
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 2 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -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<T>`](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<T>`](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<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) if you only need one level deep.
Expand Down
32 changes: 32 additions & 0 deletions source/internal.d.ts
Expand Up @@ -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';
Expand All @@ -24,6 +25,37 @@ export type BuildTuple<L extends number, Fill = unknown, T extends readonly unkn
? T
: BuildTuple<L, Fill, [...T, Fill]>;

/**
Create an object type with the given key `<Key>` and value `<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 PropertyKey, Value, CopiedFrom extends UnknownRecord = {}> =
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> = 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`.
Expand Down
3 changes: 1 addition & 2 deletions 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> = T extends string | number ? `${T}` : never;

/**
Return the part of the given array with a fixed index.
Expand Down
141 changes: 141 additions & 0 deletions 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<Configuration, 'userConfig.name'>;
// type NameConfig = {
// userConfig: {
// name: string;
// };
// Supports optional properties
type User = PickDeep<PartialDeep<Configuration>, 'userConfig.name' | 'userConfig.age'>;
// type User = {
// userConfig?: {
// name?: string;
// age?: number;
// };
// };
// Supports array
type AddressConfig = PickDeep<Configuration, `userConfig.address.0`>;
// type AddressConfig = {
// userConfig: {
// address: [{
// city1: string;
// street1: string;
// }];
// };
// }
// Supports recurse into array
type Street = PickDeep<Configuration, `userConfig.address.1.street2`>;
// type AddressConfig = {
// userConfig: {
// address: [
// unknown,
// {street2: string}
// ];
// };
// }
```
@category Object
@category Array
*/
export type PickDeep<T extends UnknownRecord | UnknownArray, PathUnion extends Paths<T>> =
T extends UnknownRecord
? Simplify<UnionToIntersection<{
[P in PathUnion]: InternalPickDeep<T, P>;
}[PathUnion]>>
: T extends UnknownArray
? UnionToIntersection<{
[P in PathUnion]: InternalPickDeep<T, P>;
}[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, Path>
: T extends UnknownRecord ? Simplify<PickDeepObject<T, Path>>
: never;

/**
Pick an object from the given object by one path.
*/
type PickDeepObject<RecordType extends UnknownRecord, P extends string | number> =
P extends `${infer RecordKeyInPath}.${infer SubPath}`
? BuildObject<RecordKeyInPath, InternalPickDeep<NonNullable<RecordType[RecordKeyInPath]>, SubPath>, RecordType>
: P extends keyof RecordType | ToString<keyof RecordType> // Handle number keys
? BuildObject<P, RecordType[P], RecordType>
: never;

/**
Pick an array from the given array by one path.
*/
type PickDeepArray<ArrayType extends UnknownArray, P extends string | number> =
// 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<InternalPickDeep<NonNullable<ArrayType[number]>, SubPath>>
: ArrayType extends readonly unknown[]
? ReadonlyArray<InternalPickDeep<NonNullable<ArrayType[number]>, SubPath>>
: never
// When `ArrayIndex` is a number literal
: ArrayType extends unknown[]
? [...BuildTuple<ArrayIndex>, InternalPickDeep<NonNullable<ArrayType[ArrayIndex]>, SubPath>]
: ArrayType extends readonly unknown[]
? readonly [...BuildTuple<ArrayIndex>, InternalPickDeep<NonNullable<ArrayType[ArrayIndex]>, 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<ArrayIndex>, ArrayType[ArrayIndex]]
: ArrayType extends readonly unknown[]
? readonly [...BuildTuple<ArrayIndex>, ArrayType[ArrayIndex]]
: never
: never;
112 changes: 112 additions & 0 deletions 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<BaseType>;
optionalString?: string;
readonly readonlyObject: {a: 1};
1: BaseType;
2?: BaseType;
};

declare const normal: PickDeep<Testing, 'string'>;
expectType<{string: string}>(normal);

type DeepType = {
nested: {
deep: {
deeper: {
value: string;
};
};
};
foo: string;
};
declare const deep: PickDeep<DeepType, 'nested.deep.deeper.value'>;
expectType<{nested: {deep: {deeper: {value: string}}}}>(deep);

type GenericType<T> = {
genericKey: T;
};
declare const genericTest: PickDeep<GenericType<number>, 'genericKey'>;
expectType<{genericKey: number}>(genericTest);

declare const union: PickDeep<Testing, 'object.number' | 'object.string'>;
expectType<{object: {number: number} & {string: string}}>(union);

declare const optional: PickDeep<Testing, 'optionalObject.optionalString'>;
expectType<{optionalObject?: {optionalString?: string}}>(optional);

declare const optionalUnion: PickDeep<Testing, 'optionalObject.string' | 'object.number'>;
expectType<{optionalObject?: {string?: string}; object: {number: number}}>(optionalUnion);

declare const readonlyTest: PickDeep<Testing, 'readonlyObject.a'>;
expectType<{readonly readonlyObject: {a: 1}}>(readonlyTest);

declare const array: PickDeep<Testing, 'object.array'>;
expectType<{object: {array: number[]}}>(array);

declare const readonlyArray: PickDeep<Testing, 'object.readonlyArray'>;
expectType<{object: {readonlyArray: readonly number[]}}>(readonlyArray);

declare const tuple: PickDeep<Testing, 'object.tuples'>;
expectType<{object: {tuples: ['foo', 'bar']}}>(tuple);

declare const objectArray1: PickDeep<Testing, `object.objectArray.${number}`>;
expectType<{object: {objectArray: Array<{a: 1; b: 2}>}}>(objectArray1);

declare const objectArray2: PickDeep<Testing, `object.objectArray.${number}.a`>;
expectType<{object: {objectArray: Array<{a: 1}>}}>(objectArray2);

declare const leadingSpreadArray1: PickDeep<Testing, `object.leadingSpreadArray.${number}.a`>;
expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>]}}>(leadingSpreadArray1);

declare const leadingSpreadArray2: PickDeep<Testing, `object.leadingSpreadArray.${number}`>;
expectType<{object: {leadingSpreadArray: [...Array<{a: 1}>, {b: 2}]}}>(leadingSpreadArray2);

declare const tailingSpreadArray1: PickDeep<Testing, 'object.tailingSpreadArray.1'>;
expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2; other: 2}}]}}>(tailingSpreadArray1);

declare const tailingSpreadArray2: PickDeep<Testing, 'object.tailingSpreadArray.1.b.c'>;
expectType<{object: {tailingSpreadArray: [unknown, {b: {c: 2}}]}}>(tailingSpreadArray2);

declare const date: PickDeep<Testing, 'object.date'>;
expectType<{object: {date: Date}}>(date);

declare const instance: PickDeep<Testing, 'object.instance'>;
expectType<{object: {instance: ClassA}}>(instance);

declare const classTest: PickDeep<Testing, 'object.Class'>;
expectType<{object: {Class: typeof ClassA}}>(classTest);

declare const numberTest: PickDeep<Testing, '1'>;
expectType<{1: BaseType}>(numberTest);

declare const numberTest2: PickDeep<Testing, '1.0'>;
expectType<{1: {0: number}}>(numberTest2);

declare const numberTest3: PickDeep<Testing, '2.0'>;
expectType<{2?: {0: number}}>(numberTest3);

0 comments on commit c60caba

Please sign in to comment.