如何让 TypeScript 类型保护对编译产生影响? [英] How to make TypeScript type guard have an effect on compilation?

查看:25
本文介绍了如何让 TypeScript 类型保护对编译产生影响?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I created a class called KVMap which inherits from Map, but with whole different types.

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

通常 Map 仅用于两种类型之间,例如 Record,但对象具有类型关联的已知键,因此我实现了这一点,但在 地图.示例:

Normally Maps are used between only two types, like Record, yet objects have known key to type associations, so I implemented just that, yet in a Map. Example:

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.

All types are optionally undefined as one can call .clear or .delete on the keys.

到目前为止一切正常.

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

There was one problem with the original type for Map: calling .has meant that you could still get compiler errors. Example:

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

这是因为 .has 只是返回一个布尔值.我试图解决这个问题.

This is due to .has simply returning a boolean. I sought to fix that.

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

I redefined .has in my inherited class as the following:

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

这应该意味着之后调用 .get 将不再有 |未定义在其类型中.

This should mean that calling .get afterward would no-longer have | undefined in its type.

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

Yet I think that the original type of this, since it contains the optional undefined, is taking precedence over the type guard union.

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

Does anyone know how I could fix this?

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

(I could attach more code, should it be necessary)

推荐答案

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

It's not clear from the handbook documentation, but when you create an intersection of two callable types, the type it produces acts like an overloaded function with two call signatures, in the same order as they appear in the intersection. And so while intuitively intersections should be commutative and in TypeScript they usually are, intersections of function types are not:

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,因为在这两种情况下,第一个调用签名都隐藏了第二个调用签名.

ns.foo() returns a number, while sn.foo() returns a string, because in both cases the first call signature hides the second call signature.

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

So the problem with your type guard this is this & { get(key: K): T[K] } is that the original this had a get() method of type Map<keyof T, T[keyof T]>["get"], and so the resulting intersection just adds a new overloaded method after that. Since the original get() method works for all keyof T inputs, it will always be selected, and so the overloaded method you added is completely hidden:

  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() 调用签名添加到 beginning 的重载列表,因此它应该优先:


Probably the easiest way to deal with this is to change the order of your intersection so that this is last. So every time you guard with has(), a true result should add the new get() call signature to the beginning of the overload list, and thus it should take precedence:

  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() 的参数是联合类型,那么您将度过一段糟糕的时光,尽管编译器向开发人员认为这是不可能的:

Please note, though, that such a type guard only makes sense if you are passing values of a single string literal type to has(). If the parameter to has() is of a union type, you will have a bad time, and you could still end up with undefined at runtime despite the compiler's assurances to the developer that it's not possible:

  // 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 代码链接

这篇关于如何让 TypeScript 类型保护对编译产生影响?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆