如何使TypeScript类型保护对编译产生影响



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

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

通常,Map只在两个类型之间使用,比如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

到目前为止一切都很好。

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,所以优先于类型保护并集。

有人知道我该怎么解决吗?

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

手册文档中并不清楚,但当您创建两个可调用类型的交集时,它生成的类型就像一个重载函数,具有两个调用签名,的顺序与它们在交集中的顺序相同。因此,虽然直观上交集应该是可交换的,并且在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()返回number,而sn.foo()返回string,因为在这两种情况下,第一呼叫签名都隐藏第二呼叫签名。


因此,类型保护this is this & { get(key: K): T[K] }的问题是,原始this有一个类型为Map<keyof T, T[keyof T]>["get"]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()时,这样的类型保护才有意义。如果has()的参数是并集类型,那么您将度过一段糟糕的时间,并且尽管编译器向开发人员保证这是不可能的,但您仍可能在运行时使用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!
}

所以要小心。


到代码的游乐场链接

最新更新