[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 string
& string | 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 f
to g
, we suddenly can't call g
with numbers
anymore.
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
R
is the contra-variant position.
TypeScript creates an intersection out of it. SO R
will 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