My goal here is to create a function named: getFields. This function has a generic <T> and a parameter ...fields: Array<keyof T>. I would like this function to return a function which when given an object of type <T> will return a reduced object with only the properties named in ...fields.

The following is one helper type and my getFields implementation:

type SubObj<T, S extends Array<keyof T>> = Pick<
  T,
  keyof { [K in S[number]]: K extends keyof T ? K : never }
>;

export function getFields<T extends Record<string, unknown>>(
  ...fields: Array<keyof T>
): (obj: T) => SubObj<T, typeof fields> {
  return (obj: T) =>
    Object.fromEntries(fields.map((field) => [field, obj[field]])) as SubObj<
      T,
      typeof fields
    >;
}

I tested this implementation with the following code:

type A = {
  a: string;
  b: string;
  c: string;
};

const b = getFields<A>('a', 'c')({ a: '', b: '', c: '' });

However, when I look at the typeof b it is Pick<A, "a" | "b" | "c">. What I really want is Pick<A, "a" | "c">.

I have tried a lot of things to make this work the way I intend it to, but the only success was adding a second generic argument which would require me to change the code to this:

const b = getFields<A, ['a','c']>('a', 'c')({ a: '', b: '', c: '' });

This is too redundant for me to see as acceptable.

At this point, I think I've hit the limits of my TypeScript abilities because I can't think of any other way to accomplish what I'm looking for.

Is this even possible to do with TypeScript? If so, what do I need to do?

1

There are 1 answers

1
jcalz On

TypeScript does not currently support partial type parameter inference (see microsoft/TypeScript#26242). If you have multiple type parameters in a type/function, you either need to specify them all explicitly, or let them all be inferred. There is no way to specify one and let the other be inferred. If you want to go that route, there are workarounds, but then this question becomes a duplicate of other such questions, and I can point to, for example, this answer for how to proceed.

Stepping back, though, I'd say that the example code here might possibly be expressed in a simpler way that doesn't require anyone specify any types at all. Consider this:

export function getFields<K extends PropertyKey>(
    ...fields: K[]
) {
    return <T extends Record<K, unknown>>(obj: T) =>
        Object.fromEntries(fields.map((field) => [field, obj[field]])) as Pick<T, K>;
}

Here we are completely removing the object type T from the call signature of getFields(). All getFields() cares about is getting a list of key-like arguments. It then returns a function which is also generic, and this one cares about the object type T, and constrains it to be something with the keys from K. Both T and K are therefore inferrable:

const b = getFields('a', 'c')({ a: '', b: '', c: '' }); 
/* const b: Pick<{
    a: string;
    b: string;
    c: string;
}, "a" | "c"> */

Since the return value of getFields() is a generic function, it can be used for types other than A, for example:

const c = getFields('a', 'c')({ a: 1, b: 2, c: 3 }) // {a: number, c: number}

But should still error if you give it something inappropriate:

const d = getFields('a', 'c')({ a: 1 }) // error! property 'c' is missing

If you really care about specifying T you can do it when calling the return function:

const e = getFields('a', 'c')<A>({ a: 1, b: 2, c: 3 }); // error!  number is not string

If for some reason you really want to specify T at the beginning and infer K, you need to use one of the workarounds for partial type inference, like even more currying:

const stricterGetFields = <T,>() => <K extends keyof T>(...fields: K[]) => (obj: T) =>
    Object.fromEntries(fields.map((field) => [field, obj[field]])) as Pick<T, K>;

const f = stricterGetFields<A>(); // specify here and do nothing else
const g = f('a', 'c')({ a: "", b: "", c: "" }); // Pick<A, 'a'|'c'>; 

Playground link to code