我试图理解自动类型规则。在这个例子中我
步骤 1、2 和 4 按预期工作。在第 4 步,打字稿清楚地知道“map”参数不能是未定义的。但在第 3 步我必须显式添加一个 ! 否则我会收到错误消息。
该代码可以工作(现在我已经添加了感叹号/断言),但它没有意义。这是 TypeScript 的正确行为吗?我的代码中做错了什么吗?3 和 4 都引用同一个变量,并且 2 是在它们之前完成的,所以我看不出有什么区别。
function parseUrlArgs(inputString: string, map?: Map<string, string>) : Map<string, string> {
if (!map) {
map = new Map();
}
//map = map??new Map(); // This has the exact same effect as the if statement, above.
// Note: JavaScript's string split would not work the same way. If there are more than two equals signs, String.split() would ignore the second one and everything after it. We are using the more common interpretation that the second equals is part of the value and someone was too lazy to quote it.
const re = /(^[^=]+)=(.*$)/;
// Note: trim() is important on windows. I think I was getting a \r at the end of my lines and \r does not match ".".
inputString.trim().split("&").forEach((kvp) => {
const result = re.exec(kvp);
if (result) {
const key = decodeURIComponent(result[1]);
const value = decodeURIComponent(result[2]);
map!.set(key, value); // Why do I need this exclamation mark?
}
});
return map;
}
Run Code Online (Sandbox Code Playgroud)
我没有更改任何 TypeScript 设置。我使用的是 Deno 内置的默认设置,此处列出。我在打字稿操场上得到了类似的结果。
这里的问题是 TypeScript 的限制:控制流分析不会传播到或传播出函数范围边界。有关详细信息和讨论,请参阅microsoft/TypeScript#9998 。还有一个更具体的问题,microsoft/TypeScript#11498,它建议能够对某些类型的回调进行“内联”控制流分析。
编译器分析代码块if (!map) { map = new Map(); }并成功理解在该块之后,mapis 绝对不是undefined,正如您可以通过尝试使用map该代码块之前和之后的方法来演示的那样:
map.has(""); // error
if (!map) {
map = new Map();
}
map.has(""); // okay
Run Code Online (Sandbox Code Playgroud)
一切都很顺利,直到您进入回调函数的主体内部,跨越函数作用域的边界:
[1, 2, 3].forEach(() => map.has("")); // error, map might be undefined
Run Code Online (Sandbox Code Playgroud)
编译器确实不知道何时或是否会调用该回调。 您知道数组forEach()为数组中的每个元素同步运行一次回调。但编译器不知道这一点,甚至不知道如何在类型系统中表示这一点(没有实现某种方法来跟踪函数对其回调执行的操作,如microsoft/TypeScript#11498中建议的那样。)
想象一下你看到了一个函数foobar(() => map.has(""))。您是否知道何时或是否会调用该回调而无需找到其实现foobar()并检查它?这就是编译器的想法forEach()。
编译器认为回调可能会在以前的控制流分析不再适用的某个时刻被调用。“也许在外部函数的其他后续部分中map被设置为undefined”因此,它放弃并视为map可能undefined。再说一次,你知道情况并非如此,因为它map超出了范围而没有被delete处理或完成map = undefined。但编译器不会花费必要的周期来解决这个问题。放弃是一种权衡,其中性能比完整性更重要。
当您意识到编译器只是假设封闭值不会在回调函数内修改时,情况会变得更糟。正如外部作用域的控制流分析不会向内传播一样,内部作用域的控制流分析也不会向外传播:
[4, 5, 6].forEach(() => map = undefined);
return map; // no error?!
Run Code Online (Sandbox Code Playgroud)
在上面的代码中,当您到达 时,map肯定会出现,但编译器允许它,并且不会发出任何警告。为什么?同样,编译器不知道回调将被调用或何时被调用。在定义或调用闭包后丢弃所有控制流分析结果会更安全,但这将使控制流分析几乎毫无用处。尝试内联回调需要了解两者有何不同,并且涉及大量工作,并且可能会导致编译器速度慢得多。假装回调不影响控制流分析是一种权衡,其中性能和便利性比健全性更重要。undefinedreturn mapforEach()foobar()
那么可以做什么呢?一件简单的事情是将您的值分配给const发生控制流分析的范围内的变量。编译器知道const变量永远不能被重新分配,并且它知道(好吧,假装)这意味着变量的类型也永远不会改变:
function parseUrlArgs(inputString: string, map?: Map<string, string>): Map<string, string> {
if (!map) {
map = new Map();
}
const resultMap = map; // <-- const assignment here
const re = /(^[^=]+)=(.*$)/;
inputString.trim().split("&").forEach((kvp) => {
const result = re.exec(kvp);
if (result) {
const key = decodeURIComponent(result[1]);
const value = decodeURIComponent(result[2]);
resultMap.set(key, value); // <-- use const variable here
}
});
return resultMap; // <-- use const variable here
}
Run Code Online (Sandbox Code Playgroud)
通过复制map到已知已定义的resultMap点,编译器知道是 类型而不是。这种类型在函数的其余部分持续存在,甚至在回调内部也是如此。这可能有点多余,但编译器可以跟踪它并且相对类型安全。mapresultMapMap<string, string>undefined
或者您可以继续使用非空运算符!。由你决定。
| 归档时间: |
|
| 查看次数: |
659 次 |
| 最近记录: |