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.

6
  • Have you tried stackoverflow.com/a/73913130/18244921? Commented Nov 10, 2022 at 14:47
  • @caTS Looks like it's a whole other topic Commented Nov 10, 2022 at 15:06
  • Why not use something like ts-deep-pick? Commented Nov 10, 2022 at 15:11
  • Because its use unstandar way for array 'books.[].id' but I need to use books.id Commented Nov 10, 2022 at 15:13
  • 1
    I came up with this mess. If it works for your use case, I can write up an answer. Commented Nov 10, 2022 at 15:54

4 Answers 4

11

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

Sign up to request clarification or add additional context in comments.

6 Comments

Looks like there is a issue for property starting with same "key" (book vs books) tsplay.dev/mppOpm I think that infer is problem
@teteyi3241 I corrected the implementation now
Wow, Is it magic ?
Is it possible to have type suggestions like normal Pick?
@BoomPT Have you tried the answer below?
|
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>;

Comments

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

Comments

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>;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.