我正在开发一个泛型函数,以类型安全和强类型的方式将字段逐个添加到未知类型的部分对象中.该函数将复制此具体行为:
type Test = { a: string, b: number, c: boolean } // The type of the object to construct
type Missing = 'a' | 'b' // The fields missing from the partially complete object
type Add = 'b' // The field to add to the partially complete object
const partial: Omit<Test, Missing> = { c: true } // Current object
const toAdd: Pick<Test, Add> = { b: 2 } // Extend with...
const result: Omit<Test, Exclude<Missing, Add>> = { ...partial, ...toAdd } // Result
上面的例子没有输入问题.函数签名将如下所示:
function add<
Type extends object,
Add extends Missing,
Missing extends keyof Type = keyof Type> (
partial: Omit<Type, Missing>,
field: Add,
value: Type[Add]
): Omit<Type, Exclude<Missing, Add>> {
// code comes here
}
const added: Pick<Test, 'b' | 'c'> = add(partial, 'b', 2)
同样,除了缺少实现之外,这不存在类型问题.显而易见的实现将是简单的:
return { ...partial, [field]: value }
但是,由于以下原因,此操作失败:
const toAdd: Pick<Type, Add> = { [field]: value } // Type '{ [x: string]: Type[Add]; }' is not assignable to type 'Pick<Type, Add>'.(2322)
好的,这是一个已知的缩小(https://github.com/microsoft/TypeScript/issues/13948)的问题-所以我需要进行强制转换,并且应该工作…不是:
const toAdd = { [field]: value } as Pick<Type, Add>
const result: Omit<Type, Exclude<Missing, Add>> = { ...partial, ...toAdd }
// Type 'Omit<Type, Missing> & Pick<Type, Add>' is not assignable to type 'Omit<Type, Exclude<Missing, Add>>'.(2322)
这显然是因为Typescript (错误地)将类型Omit<Type, Missing> & Pick<Type, Add>
与作业(job)的右侧相关联,这又一次不够窄.当Missing
和Add
不是析取的时候,错误很可能是这样的,比如{ ...{ a: string }, ...{ a: number } }
变成了{ a: number }
,而不是{ a: string & number }
,正如Typescript 所暗示的那样是{ a: never }
.
由于我正在努力避免施法,并且由于上面的已知问题已经被迫使用了一个,看起来需要另一个,但情况变得更糟了…
const result = { ...partial, ...toAdd } as Omit<Type, Exclude<Missing, Add>>
// Conversion of type 'Omit<Type, Missing> & Pick<Type, Add>' to type 'Omit<Type, Exclude<Missing, Add>>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.(2352)
我的问题是,如果我上面的判断是不正确的,我错误地预期{ ...Omit<Type, Missing>, ...Pick<Type, Add> }
应该总是Omit<Type, Exclude<Missing, Add>>
,因为Type
被约束为object
?
TL;DR
在我的实际用例中,该函数稍微复杂一些,部分对象不仅缺少字段,还可能以错误的方式包含它们,因此具体的示例如下:
type Test = { a: string, b: number, c: boolean }
type Missing = 'a' | 'b'
type Add = 'b'
const partial: Omit<Test, Missing> & OptionalRecord<Missing, unknown> = { b: null, c: true }
const toAdd: Pick<Test, Add> = { b: 2 }
const result: Omit<Test, Exclude<Missing, Add>> & OptionalRecord<Exclude<Missing, Add>, unknown> = { ...partial, ...toAdd }
,在哪里
type OptionalRecord<K extends string | number | symbol, V> = { [key in K]+?: V } // a variant of Record<K, V> where the fields are optional
在这个扩展版本中,函数签名如下所示:
function add<
Type extends object,
Add extends Missing,
Missing extends keyof Type = keyof Type>(
partial: Omit<Type, Missing> & OptionalRecord<Missing, unknown>,
field: Add,
value: Type[Add]
): Omit<Type, Exclude<Missing, Add>> & OptionalRecord<Exclude<Missing, Add>, unknown> {
// code comes here
}
,虽然我遇到了同样的问题,但此实现在没有as unknown as whatever
的情况下工作:
const toAdd = { [field]: value } as Pick<Type, Add>
const result = { ...partial, ...toAdd }
as { [key in keyof Type]: key extends Missing ? key extends Add ? Type[key] : never : Type[key] }
as Omit<Type, Exclude<Missing, Add>> & OptionalRecord<Exclude<Missing, Add>, unknown>
,没有类型不兼容的投诉.