typescript - 重新添加从具有 `Omit` 的泛型类型中删除的属性会导致类型不兼容

标签 typescript generics types

考虑以下代码:

interface Params {
  prop1 : number;
  prop2 : string;
}

function f<T extends Params>(f2 : (t : T) => void, params : Omit<T, "prop1">) {
  f2({ ...params, prop1: 4 }); // error: not assignable to type T
}

f 接受从 Params 派生的类型参数 T。参数 f2 是一个函数,它接受一个 T 类型的参数。参数 params 的类型为 T,但删除了 prop1

然后我尝试调用 f2,将一个从 params 创建的对象传递给它,并重新添加缺少的属性 prop1。但是,这会导致错误:

Argument of type 'Pick<T, Exclude<keyof T, "prop1">> & { prop1: number; }' is not assignable to parameter of type 'T'. ts(2345)

自然地,使用 T 作为 fparams 的类型并将其直接传递给 f2 会导致没有错误。

我怀疑错误是由于 Tprop1 版本不需要与 Params 指定的完全兼容(例如 T 可以将其声明为 readonly),因此通过在调用 f2 时重新添加属性,我不一定提供与 T 兼容的类型。

  • 我的分析是否正确,还是有其他原因?
  • 可以通过简单转换为 T 来修复错误:

    f2({ ...params, prop1: 4 } as T);
    

    遗憾的是,这并不能阻止我传递一个完全缺少 prop1 的对象,因为这也不会导致错误:

    f2({ ...params } as T); // wrong, but no error
    

    有没有更好的方法来解决这个问题(即不使用可能不安全的转换)?


我的TS版本是3.4.1,Omit定义如下:

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

最佳答案

I suspect the error is caused by the fact that T's version of prop1 needn't be fully compatible with what Params specifies (T could for instance declare it readonly), and so by just re-adding the property when calling f2 I haven't necessarily provided a type compatible with T.

是的,这是另一个例子:

interface ParanoidParams extends Params {
  prop1: 123;
  prop2: "abc";
  prop3: true;
}

function cb(p: ParanoidParams) {
  console.log({ 123: "yep" }[p.prop1].toUpperCase());
}

f(cb, { prop2: "abc", prop3: true }); // explodes at runtime

您可以通过添加新属性或缩小现有属性 来扩展现有对象类型;正是后一种可能性导致编译器对您的函数调用犹豫不决。编译器关心 number 可能无法分配给 T['prop1'],它会提示。因此,这是编译器按预期工作,如 microsoft/TypeScript#13442 所示。 ,关于类似问题的问题。

请注意,可以使用安全的 PickOmit 编写此类操作,但编译器仍然无法验证安全并输出相同的错误 (参见 microsoft/TypeScript#28884 示例)。所以有时候出现这样的错误的原因是语言设计者没有实现必要的高阶类型分析来查看你在做什么是安全的......要么是因为它太难实现,要么是实现起来很简单但是在相对罕见的情况下,性能影响是不值得的。

在这种情况下,通常只使用 type assertion 是合理的继续前进。即使在技术上不安全的情况下,您也可能希望使用断言:

function f<T extends Params>(f2: (t: T) => void, params: Omit<T, "prop1">) {
  f2({ ...params, prop1: 4 } as T); // judicious type assertion
}

是的,有人会传递一个 f2 的风险,它接受 Params 的病态子类型,例如 ParanoidParams,但是如果实际上这不太可能,那么您可能可以接受风险。

否则,如果您想要一个更安全的 f 调用签名来防止 ParanoidParams,您可以这样做:

function fSafer<T extends Omit<Params, "prop1">>(
  f2: (t: T & { prop1: number }) => void,
  params: T
) {
  f2({ ...params, prop1: 4 }); // okay
}

fSafer(cb, { prop2: "abc", prop3: true }); // error at compile time now

这取决于你。


好的,希望对你有帮助;祝你好运!

Link to code

关于typescript - 重新添加从具有 `Omit` 的泛型类型中删除的属性会导致类型不兼容,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57755071/

相关文章:

java - P 作为泛型中的占位符

typescript - 对象只能从 json 解析

javascript - ESLint: 8.0.0 加载插件失败 '@typescript-eslint'

regex - Angular/Typescript RegExp 枚举

typescript - 全局范围的扩充只能直接嵌套在外部模块或环境模块声明中 (2669)

java - 有和没有分配给变量的未经检查的强制转换行为

ruby-on-rails - Rails - 我应该使用哪种数据类型将数组存储在数据库中

c++ - 无法将 unsigned int 转换为 unsigned int & (Boost adjacency iterator pointer)

scala - Functor、PointedFunctor、ApplicativeFunctor 和 Monad 的确切应用是什么?

javascript - 如何在 Typescript 中为 Map 的类型别名定义索引签名?