0

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.

kaya3
  • 31,244
  • 3
  • 32
  • 61
tenbits
  • 6,468
  • 5
  • 27
  • 45
  • Please don't edit your question to include the answer - this can only confuse readers about what the question is. People can already find the answer below. I've rolled back your edit. – kaya3 Oct 12 '20 at 21:31
  • @kaya3 but my edits where not the copy past, I have added additional support for various cases - those were definitely useful for people, who finds the answere below, as the answer below has some limitations. – tenbits Oct 12 '20 at 21:35
  • Then post your own answer as an answer. It is not part of the question. – kaya3 Oct 12 '20 at 21:37

1 Answers1

1

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.

Playground link to code

jcalz
  • 125,133
  • 11
  • 145
  • 170
  • Amazing, your help is so much appreciated. I have added `(TProjection | number)` to allow `{letters: 1}`, and array support, now all our mongodb projections are type safe - [class-mongo/FindOptions.ts](https://github.com/atmajs/class-mongo/blob/9abcd23628fb1c149bff4abbb8517f6b2feb2eb9/src/types/FindOptions.ts#L20) – tenbits Oct 12 '20 at 21:23