1

I'm trying to write a TypeScript type that turns a tuple of the keys of an type into a type with only those properties. The following code is the closest I have gotten:

type Primitive = string | number | boolean;
type ExtractProps<TObject, TKeys extends (keyof TObject)[], TExtends = any> = {
    [Index in keyof TKeys as TKeys[Index] extends keyof TObject ? (TObject[TKeys[Index]] extends TExtends ? TKeys[Index] : never) : never ]:
        TKeys[Index] extends keyof TObject ? TObject[TKeys[Index]] : never 
};

type Type = {
    'string': string;
    'number': number;
    'boolean': boolean;
    'array': Array<any>
}

type Test1 = ExtractProps<Type, ['string', 'number', 'array']>;
// Output:
// type Test1 = {
//     string: string | number | any[];
//     number: string | number | any[];
//     array: string | number | any[];
// }
// Desired Output:
// type Test1 = {
//     string: string;
//     number: number;
//     array: any[];
// }
type Test2 = ExtractProps<Type, ['string', 'number', 'array'], Primitive>;
// Output:
// type Test2 = {
//     string: string;
//     number: number;
// }
// Desired Output:
// type Test2 = {
//     string: string;
//     number: number;
// }
type Test3 = ExtractProps<Type, ['string', 'number'], Primitive>;
// Output:
// type Test3 = {
//     string: string | number;
//     number: string | number;
// }
// Desired Output:
// type Test3 = {
//     string: string;
//     number: number;
// }

I can only get the type to work properly when the re-mapping expression evaluates to never for one of the keys (like array in Type2 as the property type does not extend Primitive). Is there a way of writing ExtractProps so that Type1 and Type3 have the correct property types?

dan3988
  • 13
  • 3
  • Why is the second argument a tuple instead of a union? What does the third argument do? This looks like a mixture of `Pick` and `PickByValue` as shown https://stackoverflow.com/questions/55150760/how-to-write-pickbyvalue-type – jcalz Mar 17 '22 at 21:13
  • Can you explain what the "correct" types for `Test1` and `Test3` should be? – jcalz Mar 17 '22 at 21:13
  • @jcalz There would be no way of re-mapping the keys of a union type. Using `Index in keyof TKeys` will add one property on the resulting object per item in the tuple. I've added the desired output to the code. – dan3988 Mar 18 '22 at 09:18
  • "There would be no way of re-mapping the keys of a union type"; but you don't need to remap the keys of a union type, just use the union type as the keys. Like, does [this approach](https://tsplay.dev/we4g1W) meet your needs? If not, can you explain exactly what the tuple is giving you that the union version doesn't? If so, I can write up an answer, although it really is just a combination of `Pick` and `PickByValue`. – jcalz Mar 18 '22 at 13:26
  • @jcalz Thanks, that works perfectly. I needed the tuple so I could use a rest argument to get the types, but I can get the union type easily and pass it into PickByValue. `declare function getProps(...keys: K): K extends Array ? Pick : any;` `let a = getProps('array', 'string');` – dan3988 Mar 18 '22 at 15:03

1 Answers1

0

What you're looking for seems to be a combination of the Pick<T, K> utility type which produces a type from T with only a subset of the known properties, specifically those whose keys are in K, and something we can call PickByValue<T, V> (as requested in How to write PickByValue type?) which produces a type from T with only a subset of the known properties, specifically those whose values are in V. We can use key remapping via as to write PickByValue:

type PickByValue<T, V> =
    { [K in keyof T as T[K] extends V ? K : never]: T[K] };

And then combine them to get ExtractProps:

type ExtractProps<T, K extends keyof T, V = any> =
    PickByValue<Pick<T, K>, V>

Let's test it:

type Test1 = ExtractProps<Type, 'string' | 'number' | 'array'>;
/* type Test1 = {
    string: string;
    number: number;
    array: any[];
} */

type Test2 = ExtractProps<Type, 'string' | 'number' | 'array', Primitive>;
/* type Test2 = {
    string: string;
    number: number;
} */


type Test3 = ExtractProps<Type, 'string' | 'number', Primitive>;
/* type Test3 = {
    string: string;
    number: number;
} */

Looks good.


Note that you could also implement ExtractProps directly, by combining the functionality of Pick and PickByValue without using them:

type ExtractProps<T, K extends keyof T, V = any> =
    { [P in keyof T as T[P] extends V ? Extract<K, P> : never]: T[P] 
} 

And finally, note that, as written in the question, your version of ExtractProps wants its second argument to be an arraylike type whose elements are keys. You could do that:

type ExtractPropsArray<T, KS extends Array<keyof T>, V = any> =
    PickByValue<Pick<T, KS[number]>, V>;

but the implementation does not care about the details of the array type other than the union of its element types. It just throws away most of the information about KS. I wouldn't recommend writing a type that demands information it doesn't actually need. You can always unwrap the array before you use it, like ExtractProps<T, KS[number], V>, whereas inventing a useless array wrapper when you start with a keylike type is a bit silly, like ExtractPropsArray<T, K[], V> or ExtractPropsArray<T, [K, K], V>. But that's ultimately more of an opinion than an authoritative source, so you should do it however you see fit.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360