[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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2018-11-09 [React] Persist Form Data in React and Formik with formik-persist
2017-11-09 [TypeScript] Make Properties and Index Signatures Readonly in TypeScript
2017-11-09 [Python's] Python's list comprehensions a
2016-11-09 [AngularJS] Write a simple Redux store in AngularJS app
2015-11-09 [Falcor] Indroduce to Model
2015-11-09 [Farcol] Introduce
2015-11-09 [Angualr 2] Watch for changes