Chuyển loại liên hợp thành loại giao nhau


88

Có cách nào để chuyển đổi kiểu liên hợp thành kiểu giao nhau không:

type FunctionUnion = () => void | (p: string) => void
type FunctionIntersection = () => void & (p: string) => void

Tôi muốn áp dụng một phép chuyển đổi FunctionUnionđể có đượcFunctionIntersection

Câu trả lời:


193

Bạn muốn liên minh đến giao lộ? Các kiểu điều kiện phân tán và suy luận từ các kiểu điều kiện có thể làm được điều đó. (Tuy nhiên, đừng nghĩ rằng có thể thực hiện giao nhau, xin lỗi) Đây là phép thuật xấu xa:

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

Điều đó phân phối liên minh Uvà đóng gói lại nó thành một liên minh mới, nơi tất cả các thành phần đều ở vị trí trái ngược nhau. Điều đó cho phép loại được suy ra là một giao điểm I, như đã đề cập trong sổ tay:

Tương tự như vậy, nhiều ứng cử viên cho cùng một biến loại ở các vị trí khác nhau khiến loại giao nhau được suy ra.


Hãy xem nếu nó hoạt động.

Trước tiên, hãy để tôi đặt dấu ngoặc đơn của bạn FunctionUnionvà FunctionIntersectionvì TypeScript dường như ràng buộc liên hiệp / giao điểm chặt chẽ hơn hàm trả về:

type FunctionUnion = (() => void) | ((p: string) => void);
type FunctionIntersection = (() => void) & ((p: string) => void);

Thử nghiệm:

type SynthesizedFunctionIntersection = UnionToIntersection<FunctionUnion>
// inspects as 
// type SynthesizedFunctionIntersection = (() => void) & ((p: string) => void)

Có vẻ tốt!

Hãy cẩn thận rằng nói chung UnionToIntersection<>tiết lộ một số chi tiết về những gì TypeScript nghĩ là một liên minh thực tế. Ví dụ: booleanrõ ràng là được đại diện nội bộ true | false, vì vậy

type Weird = UnionToIntersection<string | number | boolean>

trở thành

type Weird = string & number & true & false

mà trong TS3.6 + được giảm xuống một cách háo hức

type Weird = never

bởi vì không thể có giá trị là string và number và true và false .

Hy vọng rằng sẽ giúp. Chúc may mắn!


14
10x. Tôi luôn học hỏi những điều thú vị mới từ bạn. Tôi đã rất gần với câu hỏi này stackoverflow.com/questions/50369299/… nhưng thực sự cần một cách để chuyển đổi liên minh thành một giao lộ
— Titian Cernicova-Dragomir

2
Câu trả lời này thật tuyệt vời nhưng tôi thực sự cảm thấy khó hiểu cách phần này "Điều đó phân phối liên minh U và đóng gói lại nó thành một liên minh mới, nơi tất cả các thành viên đều ở vị trí trái ngược " hoạt động :( Tôi không thể hiểu hoàn toàn phần vị trí trái ngược này . .I nghĩ rằng mã này: type Param<T> = T extends (arg: infer U) => void ? U : never; type InferredParams = Param<((a: string) => void) | ((a: number) => void)>;nên cho tôi string & numbernhưng nó mang lại cho tôi string | numberbạn có thể giải thích lý do tại sao.?
— Mariusz Pawelski

10
Đó là bởi vì các tham số kiểu trần trước đó extendstrong một kiểu có điều kiện được phân phối trên bất kỳ thành phần liên hiệp nào. Nếu bạn muốn loại có điều kiện phân phối vô hiệu hóa, bạn có thể sử dụng các thủ thuật làm tham số kiểu "mặc", chẳng hạn như một tuple-yếu tố duy nhất như thế này: type Param<T> = [T] extends [(arg: infer U) => void] ? U : never;. Điều đó sẽ hoạt động theo cách bạn muốn.
— jcalz

3
@RanLottem khóa là các loại có điều kiện phân phối . Theo tôi, cuốn sổ tay giải thích nó khá tốt. Tôi đã mở rộng nó ở những nơi khác mà bạn cần thêm thông tin. Chúc may mắn!
— jcalz

1
@NicholasCuthbert hãy cẩn thận; unknownlà kết quả "đúng". Đối với tất cả Avà B, UTI<A | B>nên bình đẳng UTI<A> & UTI<B>, phải không? Hãy để Bđược nevervà lưu ý A | neverlà A. Bây giờ bạn có UTI<A> = UTI<A> & UTI<never>cho tất cả A. Cách hợp lý duy nhất để thỏa mãn điều đó là UTI<never>= unknown, vì X & unknownlà bằng X. Nếu UTI<never>là never, thì UTI<A> = UTI<A> & never, nhưng vì X & never= never, bạn đang nói UTI<A> = nevercho tất cả A... không tuyệt vời. Có thể có những trường hợp sử dụng cho UTI<never>được never, nhưng nó không rõ ràng đối với tôi, vì vậy hãy cẩn thận.
— jcalz

6

Ngoài ra còn có một vấn đề rất liên quan khi bạn muốn có một giao lộ của một số loại, nhưng không nhất thiết phải chuyển đổi liên kết thành giao lộ. Không có cách nào để đến ngay các ngã tư mà không cần đến các công đoàn tạm thời!

Vấn đề là các loại chúng tôi muốn có được một giao lộ có thể có các hợp nhất bên trong , chúng cũng sẽ được chuyển đổi thành các giao lộ. Vệ binh đến giải cứu:

// union to intersection converter by @jcalz
// Intersect<{ a: 1 } | { b: 2 }> = { a: 1 } & { b: 2 }
type Intersect<T> = (T extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never

// get keys of tuple
// TupleKeys<[string, string, string]> = 0 | 1 | 2
type TupleKeys<T extends any[]> = Exclude<keyof T, keyof []>

// apply { foo: ... } to every type in tuple
// Foo<[1, 2]> = { 0: { foo: 1 }, 1: { foo: 2 } }
type Foo<T extends any[]> = {
    [K in TupleKeys<T>]: {foo: T[K]}
}

// get union of field types of an object (another answer by @jcalz again, I guess)
// Values<{ a: string, b: number }> = string | number
type Values<T> = T[keyof T]

// TS won't believe the result will always have a field "foo"
// so we have to check for it with a conditional first
type Unfoo<T> = T extends { foo: any } ? T["foo"] : never

// combine three helpers to get an intersection of all the item types
type IntersectItems<T extends any[]> = Unfoo<Intersect<Values<Foo<T>>>>

type Test = [
    { a: 1 } | { b: 2 },
    { c: 3 },
]

// this is what we wanted
type X = IntersectItems<Test> // { a: 1, c: 3 } | { b: 2, c: 3 }

// this is not what we wanted
type Y = Intersect<Test[number]> // { a: 1, b: 2, c: 3 }

Việc thực thi trong ví dụ đã cho giống như thế này

IntersectItems<[{ a: 1 } | { b: 2 }, { c: 3 }]> =
Unfoo<Intersect<Values<Foo<[{ a: 1 } | { b: 2 }, { c: 3 }]>>>> =
Unfoo<Intersect<Values<{0: { foo: { a: 1 } | { b: 2 } }, 1: { foo: { c: 3 } }}>>> =
Unfoo<Intersect<{ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }>> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }) extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } extends any ? ((x: T) => 0) : never) | ({ foo: { c: 3 } } extends any ? ((x: T) => 0) : never)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(((x: { foo: { a: 1 } | { b: 2 } }) => 0) | ((x: { foo: { c: 3 } }) => 0)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<{ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } }> =
({ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } })["foo"] =
({ a: 1 } | { b: 2 }) & { c: 3 } =
{ a: 1 } & { c: 3 } | { b: 2 } & { c: 3 }

Hy vọng rằng điều này cũng cho thấy một số kỹ thuật hữu ích khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.