网站搜索

TypeScript - 如何用一个具有泛型类型且方法没有泛型类型参数的类来包装一个具有泛型类型方法的类?


我有以下代码示例:

class Stupid {
  private cache: Map<any, any> = new Map();
  get<T>(key: string): T {
    return this.cache.get(key);
  };
}

class Smart<T> extends Stupid {
  get(key: string): T {
    super.get<T>(key);
  }
}

我有正当的理由去封装 Stupid 类(除了泛型之外),这些理由在这个问题的精简复现中并不明显,尽管如此,我仍然想知道这是否以某种方式可行。

请注意,类 Stupid 是一个节点依赖。我无法更改该实现。

目的在于不在每个方法上使用通用类型参数,而是将其移动到封装类(类 Smart)中,并在 super 调用中使用它,以向被扩展的类(类 Stupid)提供所需的泛型。

请随意使用这个游乐场进行实验。

答案#1

Stupid 中的 get() 方法具有问题的泛型调用签名 <T>(key: string) => T,这意味着它将返回由 调用者 指定的任意类型 T 的值。这显然是不可能的:

const stupid = new Stupid();
const n = stupid.get<number>("xyz");
// const n: number
const s = stupid.get<string>("xyz");
// const s: string

在上面的例子中,你在运行时调用了两次 stupid.get("xyz"),但是编译器认为第一次调用返回的是 number,而第二次返回的是 string,这是极不可能的。这个方法的实现之所以能通过类型检查,唯一的原因是 cache.get() 的返回类型是故意不安全的 any 类型。

让我们不要过多担心 Stupid.get() 的类型安全,而是来看你在继承它时遇到的问题:


class Smart<T> extends Stupid {
  get(key: string): T { // error!
    return super.get<T>(key);
  }
}

const smart: Smart<number> = new Smart();
const n = smart.get("xyz");
// const n: number

之所以不起作用,是因为子类必须可以分配给其父类。如果 Smart<T> extends Stupid,那么每个 Smart<T> 实例 也 是一个 Stupid 实例,并且应该相应地使用:

const stupid: Stupid = smart; // this should be allowed
const s = stupid.get<string>("xyz"); 
// const s: string // uh oh

Smart<number>.get() 返回一个 number,但 Stupid.get() 返回调用者想要的任何类型,例如 string。这些行为不兼容,因此编译器会报错。如果你希望 Smart 真正成为 Stupid 的子类,你需要像 Stupid 一样把 get() 设为泛型,但这并不是你想做的。


那么我们该如何进行呢?大概我们不需要过于担心类型安全,因为无法保证 get() 会为 Stupid 或 Smart 返回正确的类型。相反,我们只需做必要的操作以使其能够编译。通常这将需要类似类型断言的东西来告诉编译器某个值属于某种类型。

这里有一种方法来做这件事:

interface Smart<T> {
  get(key: string): T
}

const FakeSmart = Stupid as new <T>() => ISmart<T>;

class Smart<T> extends FakeSmart<T> {
  get(key: string): T {
    return super.get(key);
  }
}

我们正在做的是假装 Stupid 的行为像 Smart。首先,我们创建一个 ISmart<T> 接口,它和 Stupid 一样,只是将泛型移到了你想要的位置。然后,我们将 Stupid 构造函数赋值给一个名为 FakeSmart 的新变量,并断言 FakeSmart 的类型是一个泛型构造函数。因此 FakeSmart 在运行时只是 Stupid,但编译器认为它是 Smart<T> 的一个合适的超类。

然后我们声明 Smart<T> extends FakeSmart<T>。在运行时,这与您的原始代码相同,但现在在编译时不会出现错误,因为 Smart<T> 的 get() 方法与 FakeSmart<T> 的 get() 方法兼容。请注意,调用 super.get(key) 不需要类型参数,因为 FakeSmart.get() 不需要类型参数。


就这样吧。就个人而言,我会担心像这样的代码,你可以同时告诉编译器类型的相互矛盾的东西,但如果你不是在写 Stupid 的人,那么我想你能做的最好的事情就是把它包裹在更合理的东西中。

代码的 Playground 链接