【问题标题】:TypeScript inconsistent check for undefined -- Why do I need an exclamation point here?TypeScript 对未定义的不一致检查——为什么我需要一个感叹号?
【发布时间】:2020-10-21 04:19:15
【问题描述】:

我正在尝试了解自动类型规则。在这个例子中,我

  1. 有一个带有可选参数的函数。
  2. 检查它是否未定义,如果是,请填写一个新值。
  3. 使用它。
  4. 最后归还。

第 1、2 和 4 步按预期工作。在第 4 步 typescript 清楚地知道“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;
}

我没有更改任何 TypeScript 设置。我使用的是 Deno 内置的默认设置,列出了 here。我在typescript playground 中得到了类似的结果。

【问题讨论】:

    标签: typescript deno


    【解决方案1】:

    这里的问题是 TypeScript 中的一个限制:控制流分析(如 microsoft/TypeScript#8010 中的实现和描述)不会传播到函数范围边界或传播到函数范围边界之外。有关详细信息和讨论,请参阅microsoft/TypeScript#9998。还有一个更具体的问题,microsoft/TypeScript#11498,它建议能够对某些类型的回调进行“内联”控制流分析。


    编译器分析代码块if (!map) { map = new Map(); }并成功理解在此代码块之后,map肯定不是undefined,您可以尝试在该代码块之前和之后使用map的方法来演示:

    map.has(""); // error
    if (!map) {
      map = new Map();
    }
    map.has(""); // okay
    

    一切都很顺利,直到你进入回调函数的主体,越过函数范围的边界:

    [1, 2, 3].forEach(() => map.has("")); // error, map might be undefined
    

    编译器真的不知道何时或是否会调用该回调。 知道数组forEach() 会为数组中的每个元素同步运行一次回调。但是编译器不知道这一点,甚至不知道如何在类型系统中表示这一点(没有按照microsoft/TypeScript#11498 的建议实现某种方式来跟踪函数对其回调的作用。)

    假设您看到了一个函数foobar(() =&gt; map.has(""))。如果没有找到 foobar() 的实现并对其进行检查,您是否知道何时或是否调用了该回调?这就是编译器对forEach() 的看法。

    编译器认为回调可能会在其先前的控制流分析不再适用的某个点被调用。 “也许map 在外部函数的其他稍后部分被设置为undefined”因此,它放弃并将map 视为可能undefined。同样,知道情况并非如此,因为map 超出范围而没有被deleted 或map = undefined 完成。但是编译器不会花费必要的周期来解决这个问题。放弃是一种权衡,因为性能比完整性更重要。


    当你意识到编译器只是假设一个封闭的值不会在回调函数中修改时,情况会变得更糟。就像没有来自外部范围的控制流分析向内传播一样,来自内部范围的控制流分析也没有向外传播:

    [4, 5, 6].forEach(() => map = undefined); 
    return map; // no error?!
    

    在上面的代码中,当你到达return map 时,map 肯定会是undefined,但是编译器允许它没有警告。为什么?同样,编译器不知道回调将被调用或何时调用。在定义或调用闭包之后丢弃所有控制流分析结果会更安全,但这会使控制流分析几乎无用。尝试内联回调需要了解forEach()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
    }
    

    通过将map 复制到resultMap 在已知定义map 的位置,编译器知道resultMap 的类型为Map&lt;string, string&gt; 而不是undefined。这种类型在函数的其余部分持续存在,即使在回调内部也是如此。这可能有点多余,但编译器可以跟踪它并且相对类型安全。

    或者您可以继续使用非空运算符!。这取决于你。

    Playground link to code

    【讨论】:

      【解决方案2】:

      打字稿出现问题的原因是您将新 Map 分配给相同的 map 变量。它已经确定map 的类型是Map&lt;string, string&gt; | undefined,并且该类型始终保留。

      简单的解决方法是在函数签名中创建新的 Map,以便 map 永远不会是 undefined

      function parseUrlArgs(
          inputString: string, 
          map: Map<string, string> = new Map()
      ): Map<string, string> {
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-05-30
        • 2022-08-15
        • 1970-01-01
        • 2022-08-17
        • 2012-07-07
        • 2019-03-09
        • 1970-01-01
        相关资源
        最近更新 更多