TypeScript:当合并可选类型和必需类型时,创建联合类型而不是交叉类型

4

当可选属性和必选属性通过交集进行合并时,必选属性优先

type A = { who: string }
type B = { who?: string }
// $ExpectType {who:string}
type R = A & B

可能会导致运行时错误,例如在处理函数内的默认参数模式时。
type Params = {
  who: string
  greeting: string
}

const defaults: Params = {
  greeting: 'Hello',
  who: 'Johny 5',
}

function greeting(params: Partial<Params>){
  // $ExpectType Params
  const merged = {...defaults, ...params}

  return `${merged.greeting.toUpperCase()} ${merged.who} !`
}

// @throws - TypeError: Cannot read property 'toUpperCase' of undefined
greeting({greeting:undefined, who: 'Chuck'})

问题:

我描述的是TS编译器的行为,现在问题是如何创建映射类型,将这个交叉类型解析成联合类型。

例如:

type SafeMerge<A,B>=....

// $ExpectType {greeting?: string | undefined, who?: string | undefined }
type Result = SafeMerge<Params, Partial<Params>>

混合类型示例:

// $ExpectType {one?: number | undefined, two: string, three: boolean }
type Result = SafeMerge<{one: number, two:string}, {one?: number, three: boolean}>

我非常怀疑这是可能的,但希望我错了。你的问题很有趣! - Igor Soloydenko
1个回答

5
获取一个合并类型,该类型是两种类型的综合体,每个属性都是可能性的联合体,非常简单。我们只需使用映射类型,在两个组成类型的键上进行操作:
type SafeMerge<T, U> = {
  [P in keyof T |  keyof U] : 
    | (T extends Partial<Record<P, any>> ? T[P] : never)
    | (U extends Partial<Record<P, any>> ? U[P] : never)
}
type Result = SafeMerge<{one: number, two:string }, {one?: number, three: boolean }>
// Result: 
// type Result = {
//     one: number | undefined;
//     two: string;
//     three: boolean;
// }

{{链接1:播放}}

以上解决方案的问题在于我们失去了键的可选性(也失去了只读性,这可能对这种用例来说不太重要)。同态映射类型保留修饰符,但不幸的是我们不能在此处使用它,因为我们实际上并不符合同态映射类型的任何模式({ [P in keyof T] : T[P] }{[P in K]: T[P] },其中K是具有K extends keyof T的类型参数,请参见HereHere)。
我们可以提取可选键并使用两个映射类型,一个用于任何可选键(至少在一个成分中是可选的键),另一个用于必需键:
type OptionalPropertyOf<T> = Exclude<{
  [K in keyof T]: T extends Record<K, T[K]>
    ? never
    : K
}[keyof T], undefined>

type SafeMerge<T, U> = {
  [P in OptionalPropertyOf<T> |  OptionalPropertyOf<U>]?: 
    | (T extends Partial<Record<P, any>> ? T[P] : never)
    | (U extends Partial<Record<P, any>> ? U[P] : never)
} & {
  [P in Exclude<keyof T | keyof U,  OptionalPropertyOf<T> |  OptionalPropertyOf<U>>]: 
    | (T extends Partial<Record<P, any>> ? T[P] : never)
    | (U extends Partial<Record<P, any>> ? U[P] : never)
}

type Result = SafeMerge<{one: number, two:string, three: number}, {one?: number, three: boolean}>
// Result: 
// type Result = {
//     one?: number | undefined;
// } & {
//     two: string;
//     three: number | boolean;
// }

type Id<T> = {} & { [P in keyof T]: T[P] }
type FlattenedResult = Id<SafeMerge<{one: number, two:string, three: number }, {one?: number, three: boolean}>>
// type FlattenedResult = {
//     one?: number | undefined;
//     two: string;
//     three: number | boolean;
// }

播放

可选地,我们可以使用Id将交集展平,就像我在上面的示例中所做的那样,但这是可选的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接