PickDeep and PickSelection
PickDeep
is a utility type that expands TypeScript's built-inPick
utility type to pick keys in nested objects.PickSelection
is a utility type that accomplishes the same thing but with a different interface: using selection sets (similar to GraphQL selection or Prisma's selection interface).
While both are still supported, PickSelection
is now the recommended utility type. This blog post will explain why.
What does PickDeep
do?
As noted above, PickDeep
is like Pick
but supports nested objects. Below is an example demonstrating this and explaining why it's useful.
import {PickDeep} from '@augment-vir/common';
import {assert} from 'chai';
import {test} from 'mocha';
// 1. a type with a several levels of nesting
type User = {
name: string;
address: {
streetAddress: string;
country: string;
phoneNumber: {
countryCode: string;
number: string;
};
};
};
// 2. a function using `PickDeep`
function validateUserCountryCode(
user: PickDeep<User, ['address', 'phoneNumber', 'countryCode']>,
): boolean {
return !!user.address.phoneNumber.countryCode;
}
test('validateUserCountryCode', () => {
// 3. testing the function
assert.isTrue(
validateUserCountryCode_PickDeep({
address: {
phoneNumber: {
countryCode: '1',
},
},
}),
);
});
You can try this in the TypeScript browser playground here.
Here's what's going on in that above code.
- We have a
User
type which contains several levels of nesting. - We have a function that uses
PickDeep
so its input requires only the exact data needed. - While testing that function, we only need to provide the data exactly needed by the function.
With just Pick<User, 'address'>
, we would've needed to fill in both streetAddress
and country
in the test code.
Limitations of PickDeep
Clashing sub-keys
The most obvious limitation of PickDeep
is clashing sub keys. See the following example:
import {PickDeep} from '@augment-vir/common';
type User = {
name: string;
company: {
name: string;
};
region: {
name: string;
coordinates: {
x: number;
y: number;
};
};
};
const example: PickDeep<User, ['company' | 'region', 'name' | 'coordinates']> = {
company: {
name: 'Biz',
},
region: {
name: 'Earth',
coordinates: {
x: 10,
y: 5,
},
},
};
In the above example (playground link here), if we only care about company.name
, we still have to provide region.name
.
TypeScript's recursion nerfing
However, the real killer of PickDeep
is TypeScript itself. Since my creation of PickDeep
at the beginning of 2023, the TypeScript type checker has been repeatedly nerfed in its ability to handle recursive types. The modern PickDeep
(which still works sometimes) has accordingly been nerfed to the point where it no longer supports auto completion in the keys array. Despite that, TypeScript still frequently panics when using PickDeep
with "type instantiation is excessively deep and possibly infinite" errors, sometimes even on objects that are obviously not possibly infinite (or even very deep).
An alternative: PickSelection
Due to TypeScript's arbitrary and unpredictable "excessively deep" errors, I sought out a new means entirely of accomplishing a deeply picked utility type. Thus was born PickSelection
.
The goal of PickSelection
is the same: allow picking arbitrary sub keys of a nested object. However, the method is totally different. Rather than accepting a list of key strings (as PickDeep
does), it accepts a selection set. In other words, it accepts an object of booleans with keys and nestings matching exactly (with Partial
) the original type that's being picked from. Below is an example that shows how to use PickSelection
for all the previous PickDeep
examples.
import {PickSelection} from '@augment-vir/common';
// from the first PickDeep example
type User = {
name: string;
address: {
streetAddress: string;
country: string;
phoneNumber: {
countryCode: string;
number: string;
};
};
};
function validateUserCountryCode(
user: PickSelection<User, {address: {phoneNumber: {countryCode: true}}}>,
): boolean {
return !!user.address.phoneNumber.countryCode;
}
// from the second PickDeep example
type User2 = {
name: string;
company: {
name: string;
};
region: {
name: string;
coordinations: {
x: number;
y: number;
};
};
};
const example: PickSelection<User2, {company: {name: true}; region: {coordinates: true}}> = {
company: {
name: 'Biz',
},
region: {
// notice that we no longer need to provide the region name property
coordinates: {
x: 10,
y: 5,
},
},
};
This example (playground link here) demonstrates how you can accomplish the same things as PickDeep
with a more concise and precise interface, with auto-complete support! Also, so far, TypeScript seems to be happier with PickSelection
over PickDeep
.