省略类型往返不适用于泛型

时间:2019-03-22 12:19:44

标签: typescript generics

给出广泛共享的类型Omit,其定义为:

type Omit<ObjectType, KeysType extends keyof ObjectType> =
    Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;

用于减去类型(相交的相反)或换句话说,从类型中删除某些属性。

我正在尝试使用此类型来编写一个函数,该函数将使用类型为T的对象,但其中一个属性丢失,然后设置该缺失的属性并返回类型为{{ 1}}。

使用以下使用特定类型的示例,一切都很好:

T

但是通用的完全相同的函数无法编译:

type A = { x: string }
function f(arg: Omit<A, 'x'>): A {
    return { ...arg, x: "" }
}

function g<T extends A>(arg: Omit<T, 'x'>): T { return { ...arg, x: "" } } 的定义错误是:

  

输入'Pick>&{x:string; }”不能分配给类型“ T”。

但是我确定此错误消息是错误的。类型g 可分配给Pick<T, Exclude<keyof T, "x">> & { x: string; }的。

我要去哪里错了?


更多情况下,我正在编写一个React高阶组件,该组件将接受一个组件并自动提供一些已知的道具,并返回一个删除了那些已知道具的新组件。

1 个答案:

答案 0 :(得分:2)

警告,请提前回答。摘要:

  • 根本的问题是known,但牵引力可能不太大

  • 简单的解决方法是使用类型断言return { ...arg, x: "" } as T;

  • 简单的修复方法并不完全安全,在某些情况下效果不佳

  • 在任何情况下,g()均无法正确推断T

  • 重构的g()函数可能对您来说更好

  • 我需要停止写很多东西


这里的主要问题是,编译器根本不够聪明,无法验证泛型的某些等效性。

// If you use CompilerKnowsTheseAreTheSame<T, U> and it compiles, 
//  then T and U are known to be mutually assignable by the compiler
// If you use CompilerKnowsTheseAreTheSame<T, U> and it gives an error,
//  then T and U are NOT KNOWN to be mutually assignable by the compiler,
//  even though they might be known to be so by a clever human being
type CompilerKnowsTheseAreTheSame<T extends U, U extends V, V=T> = T;

// The compiler knows that Picking all keys of T gives you T
type PickEverything<T extends object> =
  CompilerKnowsTheseAreTheSame<T, Pick<T, keyof T>>; // okay

// The compiler *doesn't* know that Omitting no keys of T gives you T
type OmitNothing<T extends object> =
  CompilerKnowsTheseAreTheSame<T, Omit<T, never>>; // nope!

// And the compiler *definitely* doesn't know that you can 
//  join the results of Pick and Omit on the same keys to get T
type PickAndOmit<T extends object, K extends keyof T> =
  CompilerKnowsTheseAreTheSame<T, Pick<T, K> & Omit<T, K>>; // nope!

为什么它不够聪明?通常,有两种大致的答案:

  • 有问题的类型分析取决于某些人类的聪明才智,而这些才智很难或不可能在编译器代码中捕获。在奇异性发生并且TypeScript编译器变得完全智能之前,您可能会想到一些事情,使编译器无法做到。

  • 所涉及的类型分析相对来说由编译器执行比较简单。但是这样做会花费一些时间,并且可能会对性能产生负面影响。它是否足以改善开发人员体验,值得付出成本?不幸的是,答案通常是不。

在这种情况下,可能是后者。有an issue in Github about it,但除非有很多人开始为它喝彩,否则我不希望看到有很多工作要做。


现在,对于任何具体类型,编译器通常都可以检查并评估所涉及的具体类型并验证等效性:

interface Concrete {
  a: string,
  b: number,
  c: boolean
}

// okay now
type OmitNothingConcrete =
  CompilerKnowsTheseAreTheSame<Concrete, Omit<Concrete, never>>; 

// nope, still too generic
type PickAndOmitConcrete<K extends keyof Concrete> =
  CompilerKnowsTheseAreTheSame<Concrete, Pick<Concrete, K> & Omit<Concrete, K>>; 

// okay now
type PickAndOmitConcreteKeys =
  CompilerKnowsTheseAreTheSame<Concrete, Pick<Concrete, "a"|"b"> & Omit<Concrete, "a"|"b">>; 

但是,在您的情况下,您尝试通过通用T使其实现,但这不会自动发生。


当您比编译器更了解所涉及的类型时,很可能需要明智地使用type assertion,这是这种情况下语言的一部分:

function g<T extends A>(arg: Omit<T, 'x'>): T {
  return { ...arg, x: "" } as T; // no error now
}

在那里,它现在可以编译,您完成了,对不对?


好吧,我们不要太草率。使用类型断言的陷阱之一是,当您确定自己在做的事情是安全的时,您告诉编译器不要担心验证某些事情。但是你知道吗?这取决于您是否希望看到一些极端情况。这是我最担心您的示例代码的地方。

假设我有一个disciminated union类型的U,这意味着 拥有一个a属性一个{ {1}}属性,具体取决于b属性的string literal值:

x

没问题吧?但是,// discriminated union U type U = { x: "a", a: number } | { x: "b", b: string }; declare const u: U; // check discriminant if (u.x === "a") { console.log(u.a); // okay } else { console.log(u.b); // okay } 扩展了U,因为类型A的任何值也应该是类型U的值。这意味着我可以这样叫A

g

// notice that the following compiles with no error const oops = g<U>({ a: 1 }); // oops is supposed to be a U, but it's not! oops.x; // is "a" | "b" at compile time but "" at runtime! 可分配给{a: 1},因此编译器认为它产生了类型为Omit<U, 'x'>的值oops。但是不是吗?您知道U在运行时既不是oops.x也不是"a",而是"b"。我们已经对编译器撒谎了,现在当我们开始使用""时会遇到麻烦。

现在也许这种情况不会发生在您身上,如果是这样,您就不必担心太多了……毕竟,打字应该使维护代码更容易,而不是更难。


最后,我想提一句,键入的oops函数将永远无法推断出比g()狭窄的T类型。如果您呼叫A,则g({a: 1})将被推断为T。如果A总是被推断为T,那么您甚至也可能没有泛型函数。

出于相同的原因,编译器无法充分窥视A来理解如何与Omit<T, 'x'>结合形成Pick<T, 'x'>,因此它无法窥视类型的值T并找出Omit<T, 'x'>应该是什么。那该怎么办?

对于编译器而言,推断您传递给它的实际值的类型要容易得多,所以让我们尝试一下:

T

现在function g<T>(arg: T) { return { ...arg, x: "" }; } 将使用类型g()的值并返回类型T的值。最终总是可以将其分配给T & {a: string},因此可以使用它:

A

如果您想以某种方式阻止const okay = g({a: 1, b: "two"}); // {a: number, b: string, x: string} const works: A = okay; // fine 的参数具有g()属性,那就没有发生:

x

但是我们可以在const stillWorks = g({x: 1}); 上设置约束来实现:

T

这是相当类型安全的,不需要类型断言,并且对于类型推断更好。所以这可能就是我要离开的地方。


好的,希望这本中篇小说对您有所帮助。祝你好运!