惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
小众软件
小众软件
Engineering at Meta
Engineering at Meta
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
罗磊的独立博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MongoDB | Blog
MongoDB | Blog
L
LINUX DO - 热门话题
酷 壳 – CoolShell
酷 壳 – CoolShell
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
Martin Fowler
Martin Fowler
G
GRAHAM CLULEY
Simon Willison's Weblog
Simon Willison's Weblog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
V
Vulnerabilities – Threatpost
云风的 BLOG
云风的 BLOG
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
I
Intezer
Scott Helme
Scott Helme
A
About on SuperTechFans
博客园 - 司徒正美
Hacker News: Ask HN
Hacker News: Ask HN
The GitHub Blog
The GitHub Blog
Forbes - Security
Forbes - Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Spread Privacy
Spread Privacy
T
Tailwind CSS Blog
S
Security Affairs
宝玉的分享
宝玉的分享

oida.dev | TypeScript, Rust

TypeScript's `erasableSyntaxOnly` Flag Unsafe for work Tokio: Macros Tokio: Channels Tokio: Getting Started Network Applications on the Tokio Stack Remake, Remodel, Reduce. The `never` type and error handling in TypeScript 5 Inconvenient Truths about TypeScript Refactoring in Rust: Introducing Traits Refactoring in Rust: Abstraction with the Newtype Pattern Announcing the TypeScript Cookbook TypeScript: Iterating over objects The road to universal JavaScript 10 years of oida.dev Rust: Tiny little traits The TypeScript converging point How not to learn TypeScript Getting started with Rust Introducing Slides and Coverage TypeScript: The humble function overload TypeScript + React: Children types are broken TypeScript: In defense of any Rust: Enums to wrap multiple errors Dissecting Deno Error handling in Rust TypeScript: Unexpected intersections Upgrading Node.js dependencies after a yarn audit TypeScript: Array.includes on narrow types TypeScript + React: Typing Generic forwardRefs shared, util, core: Schroedinger's module names Learning Rust and Go TypeScript: Narrow types in catch clauses TypeScript: Low maintenance types Tidy TypeScript: Name your generics Tidy TypeScript: Avoid traditional OOP patterns Tidy TypeScript: Prefer type aliases over interfaces Tidy TypeScript: Prefer union types over enums My new book: TypeScript in 50 Lessons Go Preact! ❤️ this in JavaScript and TypeScript TypeScript and ECMAScript Modules TypeScript + React: Why I don't use React.FC TypeScript + React: Component patterns TypeScript: Augmenting global and lib.dom.d.ts Vite with Preact and TypeScript 11ty: Generate Twitter cards automatically Are large node module dependencies an issue? TypeScript: Variadic Tuple Types Preview TypeScript: Improving Object.keys Remake, Remodel. Part 4. TypeScript + React: Typing custom hooks with tuple types TypeScript: Assertion signatures and Object.defineProperty TypeScript: Check for object properties and narrow down type Boolean in JavaScript and TypeScript void in JavaScript and TypeScript Symbols in JavaScript and TypeScript Why I use TypeScript TypeScript + React: Extending JSX Elements TypeScript: Validate mapped types and const context TypeScript: Match the exact object shape TypeScript: The constructor interface pattern Streaming your Meetup - Part 4: Directing and Streaming with OBS Streaming your Meetup - Part 3: Speaker audio Streaming your Meetup - Part 2: Speaker video Streaming your Meetup - Part 1: Basics and Projector TypeScript and React Guide: Added a new styles chapter TypeScript and React Guide: Added a new render props chapter TypeScript and React: Styles and CSS TypeScript and React TypeScript and React Guide: Added a new prop types chapter TypeScript without TypeScript -- JSDoc superpowers TypeScript: Mapped types for type maps JAMStack vs serverless web apps The Unsung Benefits of JAMStack Sites TypeScript: Ambient modules for Webpack loaders My most favourite talks in 2018 TypeScript and React Guide: Added a new context chapter TypeScript: Built-in generic types TypeScript: Type predicates JSX is syntactic sugar TypeScript and React Guide: Added a new hooks chapter Getting your CfP application right FAQ on our Angular Connect Talk: Automating UI development TypeScript and Substitutability Debugging Node.js apps in TypeScript with Visual Studio Code From Medium: Deconfusing Pre- and Post-processing From Medium: PostCSS misconceptions Saving and scraping a website with Puppeteer Cutting the mustard - 2018 edition Wordpress as CMS for your JAMStack sites My most favourite podcast episodes in 2017 My most favourite talks in 2017 My most favourite books in 2017 The Best Request Is No Request, Revisited Not so hidden figures - Organizing ScriptConf My podcast journey to ScriptCast Grid layout, grid layout everywhere! #scriptconf and #devone Object streams in Node.js
TypeScript: Union to intersection type
2020-06-29 · via oida.dev | TypeScript, Rust

Recently, I had to convert a union type into an intersection type. Working on a helper type UnionToIntersection<T> has taught me a ton of things on conditional types and strict function types, which I want to share with you.

I really like working with non-discriminated union types when I try to model a type where at least one property needs to be set, making all other properties optional. Like in this example:

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
)

const video1: Video = {
// ...
urls: {
format320p: 'https://...'
}
} // ✅

const video2: Video = {
// ...
urls: {
format320p: 'https://...',
format480p: 'https://...',
}
} // ✅

const video3: Video = {
// ...
urls: {
format1080p: 'https://...',
}
} // ✅

However, putting them in a union has some side-effects when you need e.g. all available keys:

// FormatKeys = never
type FormatKeys = keyof Video["urls"]

// But I need a string representation of all possible
// Video formats here!
declare function selectFormat(format: FormatKeys): void

In the example above, FormatKeys is never, because there are no common, intersecting keys within this type. Since I don’t want to maintain extra types (that might be error-prone), I need to somehow transform the union of my video formats to an intersection of video formats. The intersection means that all keys need to be available, which allows the keyof operator to create a union of all my formats.

So how do we do that? The answer can be found in the academic description of conditional types that have been released with TypeScript 2.8. There is a lot of jargon, so let’s go over this piece by piece to make sense out of it.

The solution #

I’ll start by presenting the solution. If you don’t want to know how this works underneath, just see this as a TL/DR.

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

Still here? Good! There is a lot to unpack here. There’s a conditional type nested within a conditional type, we use the infer keyword and everything looks like it’s way too much work that does nothing at all. But it does, because there are a couple of key pieces TypeScript treats special. First, the naked type.

The naked type #

If you look at the first conditional within UnionToIntersection<T>, you can see that we use the generic type argument as a naked type.

type UnionToIntersection<T> = 
(T extends any ? (x: T) => any : never) //...

This means that we check if T is in a sub-type condition without wrapping it in something.

type Naked<T> = 
T extends ... // naked!

type NotNaked<T> =
{ o: T } extends ... // not naked!

Naked types in conditional types have a certain feature. If T is a union, they run the conditional type for each constituent of the union. So with a naked type, a conditional of union types becomes a union of conditional types. For example:

type WrapNaked<T> = 
T extends any ? { o: T } : never

type Foo = WrapNaked<string | number | boolean>

// A naked type, so this equals to

type Foo =
WrapNaked<string> |
WrapNaked<number> |
WrapNaked<boolean>

// equals to

type Foo =
string extends any ? { o: string } : never |
number extends any ? { o: number } : never |
boolean extends any ? { o: boolean } : never

type Foo =
{ o: string } | { o: number } | { o: boolean }

As compared to the non-naked version:

type WrapNaked<T> = 
{ o: T } extends any ? { o: T } : never

type Foo = WrapNaked<string | number | boolean>

// A non Naked type, so this equals to

type Foo =
{ o: string | number | boolean } extends any ?
{ o: string | number | boolean } : never

type Foo =
{ o: string | number | boolean }

Subtle, but considerably different for complex types!

So, back in our example, we use the naked type and ask if it extends any (which it always does, any is the allow-it-all top type).

type UnionToIntersection<T> = 
(T extends any ? (x: T) => any : never) //...

Since this condition is always true, we wrap our generic type in a function, where T is the type of the function’s parameter. But why are we doing that?

Contra-variant type positions #

This leads me to the second condition:

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

As the first condition always yields true, meaning that we wrap our type in a function type, the other condition also always yields true. We are basically checking if the type we just created is a subtype of itself. But instead of passing through T, we infer a new type R, and return the inferred type.

So what we do is wrap, and unwrap type T via a function type.

Doing this via function arguments brings the new inferred type R in a contra-variant position. I will explain contra-variance in a later post. For now, it’s important to know that it means that you can’t assign a sub-type to a super-type when dealing with function arguments.

For example, this works:

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. This is co-variance.

This on the other hand, won’t work:

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

declare let f: Fun<string>
declare let g: Fun<string | number>

g = f // 💥 this cannot be assigned

And if you think about it, this is also clear. When assigning f to g, we suddenly can’t call g with numbers anymore! We miss 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: TypeScript creates an intersection out of it. 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.

How the solution works #

Let’s run it through.

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

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

// equals to

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

// we have 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

// conditional one!

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:

type FormatKeys = keyof UnionToIntersection<Video["urls"]>

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.

Further reading #

I came to this solution after digging into what contra-variant positions are and what they mean in TypeScript. Next to type system jargon, it tells us effectively that we need to provide all constituents of a generic union if used as a function argument. And this works as an intersection during the assignment.

If you want to read more on this subject, I suggest catching up on the following articles.

Related Articles