1

I have following definition of possible values by key property:

type valueByKey = { 
  key1: 'A' | 'B'; 
  key2: 'B' | 'C';
  key3: 'C' | 'D'
}

I'd like to create a type representing intersection of values by union of keys, e.g.:

type ValueIntersectionByKeyUnion<TKey> = ... 

type valueB = ValueIntersectionByKeyUnion<'key1' | 'key2'> // desired result: (('A' | 'B') & ('B' | 'C')), which is 'B'

It's pretty much simplified, but in a real world my valueByKey will contain more than a thousand of lines so I'd like to check it in compile time in order to prevent runtime surprises. I'd appreciate any ideas how to approach this by typescript types.

https://codesandbox.io/s/t58ik

fernet
  • 103
  • 1
  • 7

2 Answers2

1

You can create a type similar to UnionToIntersection. The reason we can't use that directly is that UnionToIntersection<valueByKey['key1' | 'key2']> will result in UnionToIntersection<'A' | 'B' | 'C'> which will be never.

type valueByKey = { 
  key1: 'A' | 'B'; 
  key2: 'B' | 'C';
  key3: 'C' | 'D'
}

type ValueIntersectionByKeyUnion<T,  TKey extends keyof T> = {
  [P in TKey]: (k: T[P])=>void
} [TKey] extends ((k: infer I)=>void) ? I : never

type valueB = ValueIntersectionByKeyUnion<valueByKey, 'key1' | 'key2'> 

Playground Link

Understanding UnionToIntersection will be useful in understanding this type as well, so I suggest you read jcaz's write up there.

What we have to do is create each of the function signature from each property of valueByKey and then we can apply the conditional type to contravariantly extraction the desired intersection.

For valueByKey, the first part { [P in TKey]: (k: T[P])=>void } will give us a type with the same keys but with the types as function sigantures where the original type is not the type of the parameter:

{
    key1: (k: "A" | "B") => void;
    key2: (k: "B" | "C") => void;
}

We then create a union of these function signatures using [TKey], thus resulting in ((k: "A" | "B") => void) | ((k: "B" | "C") => void).

We then apply a conditional type to extract the type of the parameter extends ((k: infer I)=>void) ? I : never. Basically we are asking the compiler what this union of function signatures is callable with, and the answer is an intersection of the parameter types, resulting in the desired intersection.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Great, that's exactly what I was looking for and trying to do but something was always missing. Thx – fernet Feb 01 '22 at 14:32
0

To get the full set of all possible keys and values in a union, use

type Intersect<T> =
    (T extends any ? (x: T) => any : never) extends
    (x: infer R) => any ? R : never
type ValueIntersectionByKeyUnion<T, TKey extends keyof Intersect<T> = keyof Intersect<T>> = T extends Record<TKey, any> ? ({
    [P in TKey]: T extends Record<P, any> ? (k: T[P]) => void : never
}[TKey] extends ((k: infer I) => void) ? I : never) : never;
type Usage = { [K in keyof Intersect<TA1>]: ValueIntersectionByKeyUnion<TA1, K> };
Arlen Beiler
  • 15,336
  • 34
  • 92
  • 135