首页 > 解决方案 > 如何让 TypeScript 类型保护对编译产生影响?

问题描述

我创建了一个名为的类KVMap,它继承自Map,但具有完全不同的类型。

class KVMap<T extends object> extends Map<keyof T, T[keyof T]> {
    ...

通常Maps 仅在两种类型之间使用,例如Record,但对象已经知道类型关联的关键,所以我实现了它,但在Map. 例子:

type T = {
    a: number,
    b: bigint,
    c: string
};

// becomes

type T = {
    get(key: "a"): number | undefined,
    set(key: "a", value: number): T,

    get(key: "b"): bigint | undefined,
    set(key: "b", value: bigint): T,

    get(key: "c"): string | undefined,
    set(key: "c", value: string): T,
};

所有类型都是可选undefined的,因为可以调用.clear.delete在键上。

到目前为止一切正常。

for 的原始类型存在一个问题Map:调用.has意味着您仍然可能会遇到编译器错误。例子:

if ( map.has(foo) ) {
    map.get(foo)() // error: object may be undefined
}

这是由于.has简单地返回一个布尔值。我试图解决这个问题。

.has在继承的类中重新定义如下:

has<K extends keyof T>(key: K): this is this & {
    get(key: K): T[K]
};

这应该意味着.get之后调用将不再具有| undefined它的类型。

然而,我认为 的原始类型this,因为它包含可选的 undefined,优先于类型保护联合。

有谁知道我该如何解决这个问题?

(如果有必要,我可以附上更多代码)

标签: typescripttypes

解决方案


从手册文档中并不清楚,但是当您创建两个可调用类型的交集时,它产生的类型就像一个具有两个调用签名的重载函数,其顺序与它们出现在交集中的顺序相同。因此,虽然直观的交集应该是可交换的,并且在 TypeScript 中它们通常是,但函数类型的交集不是

type FN = {foo(): number};
type FS = {foo(): string};

declare const ns: FN & FS;
ns.foo().toFixed(); // okay
ns.foo().toUpperCase(); // error 

declare const sn: FS & FN;
sn.foo().toFixed(); // error
sn.foo().toUpperCase(); // okay

ns.foo()返回 a number,而sn.foo()返回 a string,因为在这两种情况下,第一个调用签名都隐藏了第二个调用签名。


所以你的类型保护的问题this is this & { get(key: K): T[K] }是原来的this有一个get()type 的方法Map<keyof T, T[keyof T]>["get"],所以产生的交集只是在这之后添加了一个新的重载方法。由于原始get()方法适用于所有keyof T输入,它将始终被选中,因此您添加的重载方法完全隐藏:

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

  const v = new KVMap<Tee>();

  if (v.has("a")) {
    v.get; /* OVERLOADED:
    get(key: "a" | "b" | "c"): string | number | boolean | undefined;
    get(key: "a"): string
    */

    v.get("a").toUpperCase(); // error!

处理这个问题的最简单方法可能是改变你的交叉点的顺序,以便this最后一个。所以每次你用 保护时has()true结果应该将新的get()调用签名添加到重载列表的开头,因此它应该优先:

  class KVMap<T extends object> extends Map<keyof T, T[keyof T]> {
    declare readonly has: <K extends keyof T>(key: K) => this is {
      get(key: K): T[K]
    } & this;
  }

  const v = new KVMap<Tee>();
  if (v.has("a")) {
    v.get("a").toUpperCase(); // okay now
  }

万岁。


但是请注意,只有在将单个字符串文字类型的值传递给has(). 如果参数 tohas()是联合类型,那么您将度过一段糟糕的时光,undefined尽管编译器向开发人员保证这是不可能的,但您仍然可能在运行时结束:

  // problem
  const bc = Math.random() < 0.5 ? "b" : "c";
  const cb = (bc === "b") ? "c" : "b";
  if (v.has(bc)) {
    v.get(cb).toString(); // compiles fine, but error at runtime!
  }

所以要小心。


Playground 代码链接


推荐阅读