使用扩展泛型时,`keyof T`和`Extract <keyof T,string>`有什么区别?

时间:2019-08-29 15:13:58

标签: typescript

假设我有一个为一组数据定义有效值的接口:

interface Foo {
  bar: boolean;
}

我希望一个类能够使用一种方法公开该数据。我发现如果我使用keyof T来定义密钥,它会很好地工作:

abstract class Getter<T> {
  private data: T;

  get<K extends keyof T>(key: K): T[K] {
    return this.data[key];
  }

  abstract use(): void;
}

class ExtendedGetter<T extends Foo> extends Getter<T> {
  use() {
    this.get('bar'); // OK
  }
}

但是,将键限制为仅接受Extract<keyof T, string>的字符串会导致错误:

abstract class Getter<T> {
  private data: T;

  get<K extends Extract<keyof T, string>>(key: K): T[K] {
    return this.data[key];
  }

  abstract use(): void;
}

class ExtendedGetter<T extends Foo> extends Getter<T> {
  use() {
    this.get('bar'); // ERROR
  }          ~~~~~
}
  

“ bar”类型的参数不能分配给“ Extract ”类型的参数。 ts(2345)

还值得注意的是,在第二种情况下,如果直接使用Foo而不是使用扩展的泛型,则不会引发任何错误:

class ExtendedGetter extends Getter<Foo> { ... }

为什么会这样?

Extract<keyof T, string>keyof T之间引起错误的区别是什么?

1 个答案:

答案 0 :(得分:1)

该行为似乎被视为错误(请参见microsoft/TypeScript#24560),但我看不到任何迹象表明它将在不久的将来得到修复。

但是我倾向于将其归类为无法将值分配给未解析的conditional types的编译器类别。如果您有诸如T extends U ? X : Y之类的条件类型,并且TU是未解析的泛型类型或依赖于未解析的泛型类型,则编译器不会做太多分析来验证是否有某些值可分配给它;它只是拒绝任务:

function unresolved<T extends string>() {
  const x: [T] extends [string] ? number : number = 1; // error!
  const y: string extends T ? number : number = 1; // error!  
}

在那种情况下,即使两个条件类型几乎都必须评估为number,编译器也无法告诉您将1分配给这些类型的变量是安全的,至少从TypeScript 3.6开始。我看到a pull request可能会改善此情况,并且可能会解决您的代码,但是我只是在推测,不知道何时或是否会将其转化为语言。

只要Extract<keyof T, string>是一个未解决的泛型,T就很可能使编译器难以推理(因为Extract utility typeimplemented条件类型)。请注意,一旦将T解析为Foo之类的具体类型,然后编译器将Extract<keyof T, string>评估为具体类型"bar",那么就没有问题了,因为您看到了。


因此,解决方法。正如您指出的,您可以做的一件事就是只使用keyof T而不是Extract<keyof T, string>。尽管keyof T类型是通用的,但仍可从"bar"分配类型……编译器能够对未解析的通用类型进行 some 推理;如果类型是有条件的,这样做会更糟。如果这对您有用,那就太好了。但是,如果您想使用Extract<keyof T, string> ...

我会使用type assertion。当您对编译器不了解的值的类型有所了解时,类型断言很有用。在这种情况下,您将确保"bar"可分配给Extract<keyof T, string>,因为"bar"可同时分配给stringkeyof T。面对现实,您比编译器更聪明...而且类型断言是吹嘘自己卓越智能的好方法:

class ExtendedGetter<T extends Foo> extends Getter<T> {
  use() {
    this.get("bar" as Extract<keyof T, string>); // I'm smarter than the compiler ?
  }
}

类型断言当然应该谨慎使用,因为如果您对断言有误并且对编译器撒谎,则在运行时可能会遇到一些令人不愉快的意外。但是在这种情况下,您可以确定该断言始终有效。


好的,希望能有所帮助。祝你好运!

Link to code