[Typescript] Contra-variant type positions

Co-Variance:

declare let b: string
declare let c: string | number

c = b // ✅

// string is a sub-type of string | number
// all elements of string appear in string | number
// So we can assign b to c
// c still behaves as we originally intended it

 

Contra-variance:

But this doesn' work with function params:

type Fun<T> = (...args: T[]) => void

declare let f: Fun<string>
declare let g: Fun<string | number>
    
g = f // 💥 this cannot be assigned

 

Interesting... when we have just stringstring | number, it works

But when we wrap with function Fun<string>Fun<string | number>, it doesn't work

 

It is important to remember that you can’t assign a sub-type to a super-type when dealing with function arguments

 

If you think about, when we assign fto g, we suddenly can't call gwith numbersanymore.

We miss the part of the contract of g

This is contra-variance, and it effectively works like an intersection.

 

This is what happens when we put contra-variant positions in a conditional type:

type UnionToIntersection<T> = 
  (T extends any ? (x: T) => any : never) extends 
  (x: infer R) => any ? R : never

Ris the contra-variant position.

TypeScript creates an intersection out of it. SO Rwill be the intersection of union T.

Meaning that since we infer from a function argument, TypeScript knows that we have to fulfill the complete contract. Creating an intersection of all constituents in the union.

Basically, union to intersection.

 

Let’s run it through.

type UnionToIntersection<T> = 
  (T extends any ? (x: T) => any : never) extends 
  (x: infer R) => any ? R : never

type Format320 = { urls: { format320p: string } }
type Format480 = { urls: { format480p: string } }
type Format720 = { urls: { format720p: string } }
type Format1080 = { urls: { format1080p: string } }

type Video = BasicVideoData & (
  Format320 | Format480 | Format720 | Format1080
)

type Intersected = UnionToIntersection<Video["urls"]>

// equals to

// unwrap Video["urls"]
type Intersected = UnionToIntersection<
    { format320p: string } | 
    { format480p: string } |
    { format720p: string } |
    { format1080p: string } 
>

// T is { format320p: string }.... and T is a naked type
// this means we can do a union of conditionals

type Intersected = 
  UnionToIntersection<{ format320p: string }> |
  UnionToIntersection<{ format480p: string }> |
  UnionToIntersection<{ format720p: string }> |
  UnionToIntersection<{ format1080p: string }> 

// expand it...

type Intersected = 
  ({ format320p: string } extends any ? 
    (x: { format320p: string }) => any : never) extends 
    (x: infer R) => any ? R : never | 
  ({ format480p: string } extends any ? 
    (x: { format480p: string }) => any : never) extends 
    (x: infer R) => any ? R : never | 
  ({ format720p: string } extends any ? 
    (x: { format720p: string }) => any : never) extends 
    (x: infer R) => any ? R : never | 
  ({ format1080p: string } extends any ? 
    (x: { format1080p: string }) => any : never) extends 
    (x: infer R) => any ? R : never

// unwrap first conditional: (T extends any ? (x: T) => any : never) 

type Intersected = 
  (x: { format320p: string }) => any extends 
    (x: infer R) => any ? R : never | 
  (x: { format480p: string }) => any extends 
    (x: infer R) => any ? R : never | 
  (x: { format720p: string }) => any extends 
    (x: infer R) => any ? R : never | 
  (x: { format1080p: string }) => any extends 
    (x: infer R) => any ? R : never

// conditional two!, inferring R!
type Intersected = 
  { format320p: string } | 
  { format480p: string } | 
  { format720p: string } | 
  { format1080p: string }

// But wait! `R` is inferred from a contra-variant position
// I have to make an intersection, otherwise I lose type compatibility

type Intersected = 
  { format320p: string } & 
  { format480p: string } & 
  { format720p: string } & 
  { format1080p: string }

 

And that’s what we have been looking for! So applied to our original example:

FormatKeys is now "format320p" | "format480p" | "format720p" | "format1080p". Whenever we add another format to the original union, the FormatKeys type gets updated automatically. Maintain once, use everywhere.

 

Blog: https://fettblog.eu/typescript-union-to-intersection/

https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance

posted @ 2022-11-09 17:18  Zhentiw  阅读(30)  评论(0编辑  收藏  举报