`map.has()`为什么不起到类型保护的作用。

huangapple go评论59阅读模式
英文:

Why `map.has()` doesn't act as a type guard

问题

我面临了以下问题:在我的应用程序中,我有一个Map,其中一组lambda函数与一些Role配对。每个lambda函数都接受一个secondParameter并返回一个settings对象。

enum Role {
    ROLE_1 = "role"
}
const settingsRole1 = {}
const defaultSettings = {}
const secondParameter = ""

const map = new Map<Role, (secondParameter: string) => any>([
    [Role.ROLE_1, (secondParameter: string) => settingsRole1]
]);
const settings = map.get(Role.ROLE_1)(secondParameter) ?? defaultSettings;

在尝试调用从地图返回的函数时,我收到了以下错误:TS2722: Cannot invoke an object which is possibly 'undefined'。我尝试像下面示例中所示对其进行了保护,但似乎不起作用...

const settings = map.has(Role.ROLE_1) ? map.get(Role.ROLE_1)(secondParameter) : defaultSettings;

正如这里回答的那样,似乎使用optional chaining operator可以解决问题,因此以下代码片段实现了一个有效的保护:

const settings = map.get(Role.ROLE_1)?.(secondParameter) ?? defaultSettings;

为什么使用optional chaining operator构建的保护有效,而使用if map.has()构建的保护无效呢?

英文:

I faced the following problem: In my application I have a Map in which a set of lambda functions are paired with some Roles. Each lambda takes a secondParameter and returns a settings object.

enum Role {
    ROLE_1 = "role"
}
const settingsRole1 = {}
const defaultSettings = {}
const secondParameter = ""

const map = new Map<Role, (secondParameter: string) => any>([
    [Role.ROLE_1, (secondParameter: string) => settingsRole1]
]);
const settings = map.get(Role.ROLE_1)(secondParameter) ?? defaultSettings;

I received this error while trying to calling the function returning from the map: TS2722: Cannot invoke an object which is possibly 'undefined'.
I tried to guard it as shown below, but it doesn't seem to work...

const settings = map.has(Role.ROLE_1) ? map.get(Role.ROLE_1)(secondParameter) : defaultSettings;

As answered here, it seems that using the optional chaining operator works, so the following snippet implements a working guard

const settings = map.get(Role.ROLE_1)?.(secondParameter) ?? defaultSettings;

Why does the guard build using the optional chaining operator work, while the one built with if map.has() does not?

答案1

得分: 4

Map 上的 has() 方法 不充当 Map 实例的 类型守卫方法。因此,调用 map.has(x)map 没有 缩小范围 的效果,因此不能影响对 map.get(x) 的后续调用。

microsoft/TypeScript#13086 上有一个开放的功能请求,以支持此功能,但尚未实现,因为至少对于可变的非 ReadonlyMap(参见 https://stackoverflow.com/q/50046573/2887218 ),map.has(x) 现在为 true 并不意味着它将永远为 true。正如TS 团队开发负责人在此评论中提到的:“这比看起来要复杂得多,因为您还必须定义 has 检查应该持续多长时间。”

要对此进行任何更改,需要在下面的两个 map.get() 行中正确运行:

const map = new Map([[1, { a: 1 }], [3, { a: 2 }]]);
if (map.has(2)) {
    map.get(2).a; // 应该是可以的
    map.delete(2);
    map.get(2).a; // 不应该可以
}

现在它在这两个行上都会出错,这很烦人,但是安全的。

如果您愿意,您可以合并自己的 has() 类型守卫,也许像这样:

interface NarrowableMap<K, V> extends Map<K, V> {
    has<P extends K>(k: P): this is { get(p: P): V } & this;
}

但是然后它将不会在这两个行上出错,这很方便,但是不安全,尤其是在第二行:

const map = new Map([[1, { a: 1 }], [3, { a: 2 }]]) as
    NarrowableMap<number, { a: number }>;

if (map.has(2)) {
    map.get(2).a; // 没有错误,这是好的
    map.delete(2);
    map.get(2).a; // 没有错误,这是不好的
}

目前的语言没有一种方法可以标记方法为重置控制流缩小。属性重新分配和 delete 关键字会自动触发此操作,但是映射操作不会触发此操作。

这就是为什么它不起作用的原因。


最简单的重构是放弃 has-then-get,改为 get-then-validate。所以你可以这样做:

const v = map.get(2);
if (v !== undefined) { v.a }

或者您可以使用可选链使其更简洁:

const w = v?.a
const x = map.get(2)?.a

代码播放链接

英文:

The has() method on Map doesn't act as a type guard method on the Map instance. So calling map.has(x) has no narrowing effect on map, and therefore cannot affect subsequent calls to map.get(x).

There is an open feature request at microsoft/TypeScript#13086 to support this, but it hasn't been implemented because, at least for a mutable, non-ReadonlyMap (see https://stackoverflow.com/q/50046573/2887218 ), the fact that map.has(x) is true now doesn't mean it will be true forever. As mentioned by the TS team dev lead in this comment: "This much, much, much, much more tricky than it looks because you also have to define how long the has check should last."

Any change to accomodate this would need to act correctly in both map.get() lines below:

const map = new Map([[1, { a: 1 }], [3, { a: 2 }]]);
if (map.has(2)) {
    map.get(2).a; // should be okay
    map.delete(2);
    map.get(2).a; // should not be okay
}

Right now it errors on both of them, which is annoying, but safe.

You could merge in your own type guard for has() if you want, maybe like this:

interface NarrowableMap&lt;K, V&gt; extends Map&lt;K, V&gt; {
    has&lt;P extends K&gt;(k: P): this is { get(p: P): V } &amp; this;
}

but then it would fail to error on both of them, which is convenient, but unsafe, especially on that second line.

const map = new Map([[1, { a: 1 }], [3, { a: 2 }]]) as
    NarrowableMap&lt;number, { a: number }&gt;;

if (map.has(2)) {
    map.get(2).a; // no error, that&#39;s good
    map.delete(2);
    map.get(2).a; // no error, that&#39;s bad
}

The language currently doesn't have a way to mark methods as resetting control flow narrowing. This happens automatically on property reassignment and the delete keyword, but map operations don't trigger this.

So that's why it doesn't work.


The easiest refactoring is to give up on has-then-get and switch to get-then-validate. So you can do it like this:

const v = map.get(2);
if (v !== undefined) { v.a }

Or you could use optional chaining to make it more concise:

const w = v?.a
const x = map.get(2)?.a

Playground link to code

huangapple
  • 本文由 发表于 2023年7月31日 22:59:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76804861.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定