我正在开发一个泛型函数,以类型安全和强类型的方式将字段逐个添加到未知类型的部分对象中.该函数将复制此具体行为:

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)的右侧相关联,这又一次不够窄.当MissingAdd不是析取的时候,错误很可能是这样的,比如{ ...{ 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>

,没有类型不兼容的投诉.

推荐答案

不幸的是,TypeScript类型判断器无法对依赖于generic个类型参数(如TypeMissingAdd)的类型进行大量分析,特别是当这些类型涉及conditional types(如Exclude)和Omit时. 这类类型对于类型判断器来说基本上是opaque;它们就像是带有标签的黑盒子,确保两个盒子相同的唯一方法是它们是否具有相同的标签. 由于标签"Omit<Type, Missing> & Pick<Type, Add>"不是identical到"Omit<Type, Exclude<Missing, Add>>",我们不能期望它们被视为兼容.

人类将它们视为相同的方式是,我们可以try 像OmitPickExclude对任意输入所做的那样,并使用关于持久存在的不变量的更高阶推理.例如,Omit<T, K> & Pick<T, K>在某种意义上应该等同于T的 idea ,就是这种推理的产物.类型判断器可以验证TKspecific个实例的等价性,但它不能对任意/泛型类型进行抽象以查看等价性.有关功能请求和讨论,请参见microsoft/TypeScript#28884.

当提出这样的问题时,排版维护人员倾向于将其视为设计限制,如microsoft/TypeScript#43846,或者有时将其视为错误,如microsoft/TypeScript#37768.也许在future ,其中一些问题会得到解决,但它们可能会根据具体情况量身定做.大局可能会永远持续下go .TypeScrip不是一个proof checker/assistant,您可以让它相信类型之间新颖的抽象关系;如果它还没有被硬编码进go ,编译器将永远看不到它.

因此,如果您希望像您这样的代码能够无错误地编译,您几乎肯定需要继续使用type assertions(您将其称为"强制转换",但最好避免使用这个术语,因为它似乎会误导人们认为存在运行时效应;请改用"类型断言").这意味着您需要仔细判断您的分析是否正确或足够好,以满足您的目的(例如,在某些情况下Omit<T, M> & Pick<T, A>不等于Omit<T, Exclude<M, A>>,比如M等于string;您在乎吗?可能不会).编译器无法验证正确性,因此您必须自己承担责任.

Typescript相关问答推荐

从接口创建类型安全的嵌套 struct

为什么ESLint抱怨通用对象类型?

忽略和K的定义

在包含泛型的类型脚本中引用递归类型A<;-&B

通过按键数组拾取对象的关键点

扩展参数必须具有元组类型或传递给REST参数

在类型脚本中创建泛型类型以动态追加属性后缀

下载量怎么会超过下载量?

如何获取受类型脚本泛型约束的有效输入参数

我怎样才能正确地输入这个函数,使它在没有提供正确的参数时出错

Typescript -返回接口中函数的类型

声明遵循映射类型的接口

Angular NG8001错误.组件只能在一个页面上工作,而不是在她的页面上工作

如何静态键入返回数组1到n的函数

埃斯林特警告危险使用&as";

使用&reaction测试库&如何使用提供程序包装我的所有测试组件

Typescript泛型调用对象';s方法

两个子组件共享控制权

将函数签名合并到重载中

通过函数将对象文字类型映射到另一种类型的方法或解决方法