Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Nested Pick<T, K> in TypeScript 2.2

TypeScript supports Pick to allow you to get a "subset" object type of a given type, but there is no built-in Pick for deeper nested fields.

If you have a function that takes a large object as argument, but you don't use all of its fields, you can use Pick, Pick2, Pick3, etc to narrow down the input type to be only just what you need. This will make it easier to test your function, because when mocking the input object, you don't need to pass all fields of the "large" object.

type UserWithOnlyAddress = Pick<User, 'address'>;
type User = {
id: number,
name: string,
address: {
street: string,
zipcode: string,
geo: {
lat: string,
lng: string,
},
},
};
type UserWithOnlyStreetAddress = Pick2<User, 'address', 'street'>;
type User = {
id: number,
name: string,
address: {
street: string,
zipcode: string,
geo: {
lat: string,
lng: string,
},
},
};
type Pick2<T, K1 extends keyof T, K2 extends keyof T[K1]> = {
[P1 in K1]: {
[P2 in K2]: (T[K1])[P2];
};
};
type UserWithOnlyGeoLat = Pick3<User, 'address', 'geo', 'lat'>;
type User = {
id: number,
name: string,
address: {
street: string,
zipcode: string,
geo: {
lat: string,
lng: string,
},
},
};
type Pick3<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]> = {
[P1 in K1]: {
[P2 in K2]: {
[P3 in K3]: ((T[K1])[K2])[P3];
};
};
};
@millsp
Copy link

millsp commented Jun 19, 2019

Hi @staltz, your code really inspired me so I thought this was worth sharing.

import {O} from 'ts-toolbelt'

type O = { // A binary tree
    a: {
        a: {
            a: 'aaa'
            b: 'aab'            
        }
        b: {
            a: 'aba'
            b: 'abb'            
        }
    }
    b: {
        a: {
            a: 'baa'
            b: 'bab'            
        }
        b: {
            a: 'bba'
            b: 'bbb'            
        }
    }
}

// O.P stands for Object.Path

type pickDeep0 = O.P.Pick<O, ['a', 'b', 'b']> // {...aab

type pickDeep1 = O.P.Pick<O, ['a', 'b', 'a' | 'b']> // {...aba, abb

type pickDeep2 = O.P.Pick<O, ['a', 'a' | 'b', 'a' | 'b']> // {...aba, abb; aaa, aab

type pickDeep3 = O.P.Pick<O, ['a' | 'b', 'a' | 'b', 'a' | 'b']> // all

type pickDeep4 = O.P.Pick<O, ['a' | 'b']> // all

It uses the technique you describe right above with a recursive mapped type that consumes the path.
The hard part was to get TypeScript to actually compute the mapped type deeply (doesn't by default).

And this opened the door to n-depth object traversing. I have then implemented Merge, Update, Omit at any depth (and without effort).
I first wrote it as a recursive type (not mapped) then I found your code and re-wrote it to a mapped type. It improved performance by x6.

So I thanked you on the project's page. If you're interested, I would appreciate your feedback :)

@staltz
Copy link
Author

staltz commented Jun 19, 2019

Hi! Nice project, it seems very well documented. I also released this typescript util as a library https://github.com/staltz/ts-multipick

Cheers

@arboleya
Copy link

arboleya commented Jun 24, 2021

@staltz Really amazing insight! Thank you very much for this.

@millsp Man, what a mind-blowing project! Jaw-dropping, to say the least. Congrats on such a feat!

@iSplasher
Copy link

iSplasher commented Jul 7, 2021

I managed to implement a combined version of the built-in Pick with nested support.
It only supports a depth of 1 but I'm sure it can be generalized to any depth.

type NestedPick<
  T,
  K extends keyof T | unknown,
  KN extends keyof T,
  KNK extends keyof T[KN]
> = Pick<T, K> & { [Key in KN]: Pick<T[KN], KNK> };


interface Address {
  name: string;
  ignore: number;
}

interface User {
  name: string;
  ignore: number;
  address: Address;
  address2: Address;
}

type T = NestedPick<User, "name", "address", "name"> // {name: '', address: {name: ''}}

// if you have multiple nested keys that you want to extract then you can just
type T2 = NestedPick<User, unknown, "address2", "name"> // {address2: {name: ''}}
type T3 = T & T2 // {name: '', address: {name: ''}, address2: {name: ''}}

@millsp would be nice if ts-toolbelt could include something like this

update

I updated it to now also support arrays

// unwrap up to one level
type Unarray<T> = T extends Array<infer U> ? U : T;

type NestedPick<
  T,
  K extends keyof T | unknown,
  KN extends keyof T,
  KNK extends keyof Unarray<T[KN]>
> = Pick<T, K> &
  {
    [Key in KN]: T[KN] extends Array<infer U>
      ? Pick<U, KNK>[]
      : Pick<T[KN], KNK>;
  };

interface Address {
  name: string;
  ignore: number;
}

interface User {
  name: string;
  ignore: number;
  address: Address;
  address2: Address[];
}

type T = NestedPick<User, 'name', 'address2', 'name'>; // {name: '', address2: [ {name: ''} ]}

@iSplasher
Copy link

iSplasher commented Jul 7, 2021

Found a library that does this well: https://www.npmjs.com/package/ts-deep-pick

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment