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];
};
};
};
@pirix-gh

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Owner Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.