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

KOV*_*IKO 5 typescript

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

interface Foo {
  bar: boolean;
}
Run Code Online (Sandbox Code Playgroud)

我希望一个类能够使用方法公开该数据。我发现如果我用它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
  }
}
Run Code Online (Sandbox Code Playgroud)

但是,将键限制为仅接受以下字符串会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
  }          ~~~~~
}
Run Code Online (Sandbox Code Playgroud)

类型“bar”的参数不可分配给类型“Extract<keyof T, string>”的参数。TS(2345)

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

class ExtendedGetter extends Getter<Foo> { ... }
Run Code Online (Sandbox Code Playgroud)

为什么会出现这种情况?

Extract<keyof T, string>和之间有什么区别keyof T导致错误?

jca*_*alz 4

看起来这种行为被认为是一个错误(请参阅microsoft/TypeScript#24560),但我没有看到任何表明它会在不久的将来得到修复的信息。

但我倾向于将其归入编译器无法为未解析的条件类型赋值的类别。T extends U ? X : Y如果您有像and 或Tor之类的条件类型,它们U是未解析的泛型类型或依赖于未解析的泛型类型,则编译器不会进行太多分析来验证某个值是否可分配给它;它大多只是拒绝分配:

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

在这种情况下,即使两种条件类型几乎都必须计算为number,编译器也无法判断分配给这些类型的变量是安全的1,至少从 TypeScript 3.6 开始是这样。我看到一个拉取请求可能会改善这一点,并且可能会解决您的代码,但我只是推测,我不知道它何时或是否会进入该语言。

可以说,Extract<keyof T, string>何时T是一个未解析的泛型,编译器可能很难推理(因为Extract实用程序类型是作为条件类型实现的)。请注意,onceT被解析为具体类型,如Foo,然后Extract<keyof T, string>由编译器评估为具体类型"bar",并且没有问题,如您所见。


所以,解决方法。正如您所指出的,您可以做的一件事就是仅使用keyof T而不是Extract<keyof T, string>. 尽管该类型keyof T是泛型的"bar",但已知该类型可以从 分配...编译器能够对未解析的泛型类型进行一些推理;当类型是有条件的时,这样做会更糟糕。如果这对你有用,那就太好了。但如果你想使用Extract<keyof T, string>...

我会使用类型断言。当您了解编译器不知道的值的类型时,类型断言非常有用。在这种情况下,您确定"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 
  }
}
Run Code Online (Sandbox Code Playgroud)

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


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

链接到代码