Небольшой вариант StrictUnion
найден здесь будет хорошо работать:
type UnionKeys<T> = T extends T? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends T? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
function example(props: StrictUnion<{ id: number } & ({ name: string } | { age: number })>) {
const { id, name, age } = props
}
Playground Link
The Way StrictUnion
работа заключается в обеспечении всех членов союза всех членов от всех членов союза. Это обеспечивается добавлением любых членов, которые отсутствуют с типом undefined
. Так что этот тип: { id: number } & ({ name: string } | { age: number })
станет таким: { id: number; name: string; age: undefined } | { id: number; name: undefined; age: number }
. Поскольку этот новый тип имеет ту же структуру, мы можем деструктурировать его.
Чтобы построить StrictUnion
, мы должны сначала получить объединение всех ключей от всех составляющих объединения. Для этого мы должны использовать дистрибутивное поведение условных типов. Используя это поведение, мы можем создать тип, который извлекает ключи каждой составляющей объединения и создает объединение всех. Для запуска распределительного поведения мы можем использовать всегда истинное условие (T extends T
, T extends unknown
или, менее идеальное T extends any
). С этим мы получаем следующий тип извлечения всех ключей:
type UnionKeys<T> = T extends T ? keyof T : never;
Ниже мы можем увидеть, как этот тип применяется:
type A = { id: number; name: string }
type B = { id: number; age: number }
UnionKeys<A | B>
// Conditional type is applied to A and B and the result unioned
<=> (A extends unknown ? keyof A: never) | (B extends unknown ? keyof B: never)
<=> keyof A | keyof B
<=> ("id" | "name") | ("id" | "age")
<=> "id" | "name" | "age"
После того, как у нас есть UnionKeys
, мы можем использовать другой дистрибутивный условный тип для go через каждого члена объединения и посмотрите, какие ключи отсутствуют в данном типе T
(с использованием Exclude<UnionKeys<TAll>, keyof T>
) и пересекают исходный T
с Partial
Record
, который содержит эти клавиши набраны как undefined
. Нам нужно дважды передать объединение в дистрибутивный тип, один раз для распределения по (T
) и один раз, чтобы весь союз мог извлечь ключи, используя UnionKeys
.
. посмотрите, как применяется этот тип:
type A = { id: number; name: string }
type B = { id: number; age: number }
StrictUnion<A | B>
<=> StrictUnionHelper <A | B, A | B>
// Distributes over T
<=> (A extends A ? A & Partial<Record<Exclude<UnionKeys<A | B>, keyof A>, undefined>> : never) | (B extends B ? B & Partial<Record<Exclude<UnionKeys<A | B>, keyof B>, undefined>> : never)
<=> (A extends A ? A & Partial<Record<Exclude<"id" | "name" | "age", "id" | "name">, undefined>> : never) | (B extends B ? B & Partial<Record<Exclude<"id" | "name" | "age", "id" | "age">, undefined>> : never)
<=> (A extends A ? A & Partial<Record<"age", undefined>> : never) | (B extends B ? B & Partial < Record < "name" >, undefined >> : never)
// The condition A extends A and B extends B are true and thus the conditional type can be decided
<=> (A & Partial<Record<"age", undefined>>) | (B & Partial<Record<"name">, undefined>>)
<=> { id: number; name: string; age?: undefined } | { id: number; age: number; name?: undefined }