fernet
fernet

Reputation: 103

Intersection of values based on union of keys

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

Upvotes: 1

Views: 274

Answers (2)

Arlen Beiler
Arlen Beiler

Reputation: 15906

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> };

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250186

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.

Upvotes: 1

Related Questions