How to implement deep pick in typescript
Asked Answered
P

4

6

I would like to implement deep pick in typescript.

My example code is:

interface TestBook {
    id: string;
    name: string;
}
interface TestUser {
    id: string;
    email: string;
    books: TestBook[];
}

I and I would like to use deep pick like:

const foo: DeepPick<TestUser, 'id' | 'books.name'> = {...
/*

{
  id: ..
  books: [{name: ...}]
}

*/

Problem: There is only Pick in standard typescript and there is no library implement this DeepPick.

How can I do it? Which technic should I use?

I tried to find on google and SO.

Pshaw answered 10/11, 2022 at 14:5 Comment(6)
Have you tried https://mcmap.net/q/1770849/-defined-typescript-type?Exultant
@caTS Looks like it's a whole other topicPshaw
Why not use something like ts-deep-pick?Exultant
Because its use unstandar way for array 'books.[].id' but I need to use books.idPshaw
I came up with this mess. If it works for your use case, I can write up an answer.Exultant
Its cool, working!Pshaw
E
9

Let's first define some utility types to get the "head" or "tail" of a path:

type Head<T extends string> = T extends `${infer First}.${string}` ? First : T;

type Tail<T extends string> = T extends `${string}.${infer Rest}` ? Rest : never;

Then our DeepPick can take the heads of the paths and then deep pick the tail:

type DeepPick<T, K extends string> = T extends object ? {
  [P in Head<K> & keyof T]: T[P] extends readonly unknown[] ? DeepPick<T[P][number], Tail<Extract<K, `${P}.${string}`>>>[] : DeepPick<T[P], Tail<Extract<K, `${P}.${string}`>>>
} : T

If it's not an object, we shouldn't do anything to it. Inside the mapped type, I also added a case for arrays.

Playground

Exultant answered 10/11, 2022 at 18:9 Comment(6)
Looks like there is a issue for property starting with same "key" (book vs books) tsplay.dev/mppOpm I think that infer is problemPshaw
@Pshaw I corrected the implementation nowExultant
Wow, Is it magic ?Busily
Is it possible to have type suggestions like normal Pick?Clarita
@BoomPT Have you tried the answer below?Exultant
This is brilliant. Would you know how to unwrap the final object instead eg: "user.friends.age" to return the type as number[] versus: { user: { friends: [{name: "bob", age: 50}] } } ?Frierson
K
2

I tried @zenly's version and found I was losing type hints, so I've modified it slightly. Here's a more opinionated version (zenly's handles a wider set of inputs) that persisted typehints better

type Head<T extends string> = T extends `${infer First}.${string}` ? First : T;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type Tail<T extends string> = T extends `${string}.${infer Rest}` ? Rest : never;

type DeepPick<TObject, TKey extends string> = UnionToIntersection<TObject extends object
    ? TKey extends `${infer A}.${infer B}`
        ? {
              [P in Head<TKey> & keyof TObject]: DeepPick<TObject[P], Tail<Extract<TKey, `${P}.${string}`>>>;
          }
        : TKey extends keyof TObject
        ? Pick<TObject, TKey>
        : never
    : TObject>;
Kranz answered 3/5, 2023 at 21:26 Comment(0)
T
1

@zelsny answer is amazing, but it fails with arrays that can be null or undefined (T[] | null | undefined). This is a solution I found:

type DeepPick<T, K extends string> = T extends object
  ? NonNullable<T> extends readonly unknown[]
    ? DeepPick<NonNullable<T>[number], K>[] | Exclude<T, NonNullable<T>>
    : {
        [P in Head<K> & keyof T]: DeepPick<
          T[P],
          Tail<Extract<K, `${P}.${string}`>>
        >
      }
  : T

Playground

Thrombophlebitis answered 10/9, 2023 at 17:42 Comment(0)
C
0

All of the answers above provide various ways to achieve similar purposes. Our team needed a way to get a slice of an object while still preserving autocomplete / intellisense.

What we ended up with is a somewhat healthy mix of the previous answers. DeepPick is defined at the end of the snippet and you get DeepKeyOf as a bonus.

/**
 * Converts a union type to an intersection type
 *
 * @template U - Union type
 */
type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

/**
 * Extracts any top-level and/or deep key of an object
 *
 * @remarks Returns never if T is not an object
 *
 * @template T - The object
 */
export type DeepKeyOf<TObject> = TObject extends object
  ? {
      [K in keyof TObject & (string | number)]: TObject[K] extends object
        ? `${K}.${DeepKeyOf<TObject[K]>}` | K
        : K;
    }[keyof TObject & (string | number)]
  : never;

/**
 * Picks a deep slice of an object
 *
 * @remarks Does not have autocomplete. use {@link DeepPick}
 *
 * @template TObject - The object
 * @template {string | number} TKey - The key
 */
type DeepPickLax<TObject, TKey extends string | number> = UnionToIntersection<
  TObject extends object
    ? TKey extends `${infer THead}.${infer TTail}`
      ? {
          [P in THead & keyof TObject]: TTail extends DeepKeyOf<TObject[P]>
            ? DeepPickLax<TObject[P], TTail>
            : never;
        }
      : TKey extends keyof TObject
      ? Pick<TObject, TKey>
      : never
    : TObject
>;

/**
 * Superset of {@link DeepPickLax} with stricter TKey type argument
 *
 * @remarks Use this instead of {@link DeepPickLax}
 *
 * @template {object} TObject - The object
 * @template {DeepKeyOf<TObject>} TKey - The key
 */
export type DeepPick<
  TObject extends object,
  TKey extends DeepKeyOf<TObject>
> = DeepPickLax<TObject, TKey>;
Calder answered 29/5, 2024 at 16:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.