Reputation: 8008
Assume we have an object
let obj = {
foo: 'foo',
bar: 'bar',
letters: {
a: 'some a',
b: 'some b',
}
}
and we want to do a deep pick by a definition:
let projection = {
foo: 1,
letters: {
b: 1
}
}
and it would produce:
let result = {
foo: 'foo',
letters: {
b: 'some b'
}
}
The question is not how to do this in javascript - it is quite straightforward. But how the TypeScript definition would look like.
type TProjection<T extends object, U extends keyof T> = {
[key in U]?: T[key] extends object ? TProjection<T[key], keyof T[key]> : number;
}
function pick<T extends object, U extends keyof T>(
obj: T,
projection: TProjection<T, U>
): Pick<T, U> {
return null;
}
This results to top-level pick only, the letter
field includes both a
and b
.
Upvotes: 1
Views: 531
Reputation: 328503
The implementation of your pick()
function might matter a bit, since there are probably edge cases in the typing I will propose here that you'll need to test. Hopefully you can use this as a starting point and tweak to get edge cases.
First it's useful to describe a Projection<T>
which is the acceptable type of projection
values for an obj
of type T
:
type Projection<T extends object> = {
[K in keyof T]?: T[K] extends object ? Projection<T[K]> : number
};
(We could say that number
should be 1
but then you'll need something like a const
assertion for your projection
variable.)
Given an object type T
and a projection type P
that extends Projection<T>
, we can now represent DeepPickByProjection<T, P>
:
type DeepPickByProjection<T extends object, P extends Projection<T>> = {
[K in Extract<keyof T, keyof P>]: (
P[K] extends number ? T[K] :
T[K] extends object ? DeepPickByProjection<T[K], P[K]> :
never
) } extends infer O ? { [K in keyof O]: O[K] } : never;
Basically we are walking through the common keys of T
and P
. If the property in P
is a number
, we output the property from T
. If it is an object
, then we recurse down into T[K]
and P[K]
and repeat. The only part that might look weird is the extends infer O...
bit at the end. This is a trick to force the compiler to eagerly evaluate the output type instead of deferring it. If you want an output type like {foo: string; letters: {b: string}}
instead of DeepPickByProjection<UglyTypeOne, UglyTypeTwo>
, you need something like this trick. See this question for details about how it works.
Okay, let's see if the typing works:
function pick<T extends object, P extends Projection<T>>(
obj: T,
projection: P
): DeepPickByProjection<T, P> {
return null!;
}
const result = pick(obj, projection);
/* const result: {
foo: string;
letters: {
b: string;
};
} */
Looks good. The output type of result
is the same as the input type of obj
filtered by the projection. So hooray.
Remember though there might be lots of edge cases. I'd be concerned about optional properties, union properties, and array-valued properties for starters, as well as making sure someone wouldn't want to pass in something like {letters: 1}
meaning "copy the whole subtree here", since the above typing prohibits that, I think. As I said, this should be thought of as a launching point and not as a production-ready drop-in for your use case.
Upvotes: 2