【问题标题】:Why Number.isFinite dont have type guard?为什么 Number.isFinite 没有类型保护?
【发布时间】:2021-04-19 13:02:10
【问题描述】:

有人能问我为什么Number.isFinite 没有像number is number 这样的类型保护吗?

这个例子提供了一个错误Object is possibly 'undefined'

function inc (n?: number) {
  return Number.isFinite(n) ? n + 1 : 1;
}

游乐场:

https://www.typescriptlang.org/play?ssl=1&ssc=1&pln=3&pc=2#code/GYVwdgxgLglg9mABDSiAUYD8AuRYQC2ARgKYBOAlIgN4BQiiZJUIZSAcoaWQHQwDOAMRQwoJDFUx5EAakQBGRLnkBuWgF8gA

【问题讨论】:

    标签: javascript typescript


    【解决方案1】:

    首先是一些背景。乍一看,这似乎与 JavaScript 之间的区别有关

    isFinite
    

    它将首先将其参数转换为数字和

    Number.isFinite
    

    它很乐意接受所有参数,无论类型如何,并且只为有限数字返回true(没有隐式转换)。在 TypeScript 中,这些有签名

    function isFinite(number: number): boolean
    
    (method) NumberConstructor.isFinite(number: unknown): boolean
    

    在第二种方法中,参数类型是unknown,而不是number。原因是在 JavaScript 中,Number.isFinitehappy 为非数字返回 false(而 isFinite 首先强制)。

    > Number.isFinite(false)
    false
    > isFinite(false)
    true
    > Number.isFinite(Symbol(2))
    false
    > isFinite(Symbol(2))
    Uncaught TypeError: Cannot convert a Symbol value to a number at isFinite (<anonymous>)
    

    现在,由于 Number.isFinite 已经期望接受来自任何类型的所有参数并返回 false,无论参数多么古怪,在 TypeScript 中保留这种行为是有意义的。因此正确的参数类型是unknown。对于仅适用于数字的全局 isFinite(以及 TypeScript 帮助我们避免的强制),将参数限制为 number 是有意义的。

    但是正如您的问题所问的那样,为什么,鉴于 Number.isFinite 将接受任何参数并仅针对数字返回 true,为什么它不能成为类型保护?原因是它为 some 返回 true,而不是 all 个数字!让我们至少尝试编写自己的守卫:

    function numberIsFinite(n: any): n is number {
        return Number.isFinite(n)
    }
    

    这些是有道理的:

    console.log(numberIsFinite(20)) // true
    console.log(numberIsFinite(Number.MAX_VALUE)) // true
    console.log(numberIsFinite(undefined)) // false
    console.log(numberIsFinite("still ok")) // false
    

    但这就是事情出错的地方:

    console.log(numberIsFinite(Infinity)) // false but uh-oh
    console.log(numberIsFinite(NaN)) // false but uh-oh
    

    现在确实有了这种类型保护

    const x: number|string = 3
    if (numberIsFinite(x)) {
      console.log(`A number whose type is ${typeof(x)}`)
    } else {
      console.log(`A string whose type is ${typeof(x)}`)
    }
    

    将打印

    "A number whose type is number" 
    

    到目前为止还不错,但是:

    const x: number|string = Infinity
    if (numberIsFinite(x)) {
      console.log(`A number whose type is ${typeof(x)}`)
    } else {
      console.log(`A string whose type is ${typeof(x)}`)
    }
    

    将打印

    "A string whose type is number" 
    

    这很愚蠢。

    如果 TS 中有一个称为“有限数”的依赖类型,那么(也许只有那时)将 Number.isFinite 设置为类型保护是有意义的。

    感谢@VLAZ,此解释基于其答案。在阅读了他们的答案后,我将第二部分添加到我的原始答案中。

    【讨论】:

    • 说得很好。从中学到了不少东西,谢谢!
    • 这是有用的信息,但 OP 询问为什么 Number.isFinite() 不是类型保护。据推测,如果你这样做if (Number.isFinite(x)) x += 2;,那总是正确的。没有Number.isFinite 将返回true 但参数不是数字类型的实例。
    • @RayToal 原因基本上是Number.isFinite() 返回false,该值仍然可以是一个数字。我的答案的初稿是当我以某种方式多次阅读isFinite,但不知何故将其视为isInteger。不得不用一个更适合非有限值的例子来重写它。
    • 我看到你的答案暂时被删除和重写,干得好。我也在为后代更新我的答案,但你的答案应该被接受。
    【解决方案2】:

    Number.isFinite() 不是类型保护,因为它会在极端情况下导致奇怪和错误的行为。

    Number.isFinite(4) -> true 并且值是一个数字。没关系。
    Number.isFinite("apple") -> false 并且该值不是数字。没关系。
    Number.isFinite(Infinity) -> false 但值不是不是数字。这不好。

    JavaScript 中的某些内容属于 number 类型,但不是有限的。这些是值Infinity、-Infinity 和NaN。是的,它代表“不是数字”,但它是数字类型。这很令人困惑,但很有意义。1

    在大多数情况下,我们并不关心这些非限定值。

    但是,由于它们仍然是数字,因此它们在数学运算中有效。无穷大的概念也是数学上的,有时很有用。

    丢弃这些值有时会导致错误的代码。假设Number.isFinite() 被声明为类型保护。然后考虑这段代码:

    function atLeast(minValue: string | number, x: number): boolean {
        const min: number = Number.isFinite(x)
            ? minValue
            : Number(minValue.replace(/\D/g, "")); //convert
        
        return num >= min;
    }
    
    atLeast(2, 42);            //true
    atLeast("$3.50", 42);      //true
    atLeast(-Infinity, 42);    //error
    

    这将是有效的代码。错误的逻辑,有点奇怪,但这里只是为了说明一点。不过,更现实的用法可能是:

    let dynamicMinimum = -Infinity; //allow all
    /* ... */
    someArray.filter(x => atLeast(dynamicMinimum, x));
    

    函数中的代码被 TypeScript 接受并且当Number.isFinite() 返回false 类型缩小会导致minValue 不会被视为数字,只允许被视为字符串由编译器。

    虽然此处的示例是人为设计的,而且不难发现它是错误的,但您很容易陷入现实世界的情况,当它仍然是一个数字时,您不小心将一个值缩小为一个数字。这就是为什么Number.isFinite() 不是类型保护的原因——它可能会错误地断言一些具有误导性的类型。


    1 我希望这是一个直观的解释,为什么NaN 是一个数字类型。在 JavaScript 中,值可以更改,并且某些数学运算需要使用一个数字,否则它们没有意义。但并非所有内容都转换为数字。因此,当您将值转换为数字时,您需要以某种方式表示它无法转换。这就是NaN 的用武之地。

    由于 NaN 在转换之前的上下文信息,你不能肯定地说它等于任何东西,因此为什么 NaN == NaN 是 false。等效代码可能是 Number("apple") == Number("orange") - 这两个值在转换之前显然不相等。

    【讨论】:

    • 这是一个很好的答案。顺便说一句,“并非所有内容都可以转换为数字”,这是一个很好的观点,在这里发挥作用的不仅仅是NaN。尝试将符号转换为数字会引发 TypeError。
    • @RayToal 是的……符号很奇怪。不一定是因为它们错了,它们只是不像其他所有东西那样工作得很好。这非常正确 - 对某些操作使用符号超出了无意义的范围 "apple" * "orange" 是一个无意义的操作 - 语法上有效但垃圾输入导致垃圾输出。对于符号,数学运算是一个严重的错误。不仅仅是垃圾输入 - 它是无效的。这是好,但随后会导致与现有处理事物的方式不同。 BigInt 的工作方式也类似2n * 4 会导致错误。
    【解决方案3】:

    因为在打字稿中没有办法返回一个结合一些保护条件的值。你能做的是 a)为未定义的值添加额外检查(但我认为在您的情况下,您可能不再需要 Number.isFinite)

    function inc (n?: number) {
        return typeof n === "number" && Number.isFinite(n) ? n + 1 : 1;
    }
    

    b) 或编写自己的 isFiniteNumber 守卫。

    【讨论】:

    • a) 但是 Number.isFinite 已经明确定义了数字是通过 b) 嗯...
    • 为什么不Number.isFinite(n) ? n! + 1 : 1
    • 改进了第一次检查以接受 0 值
    • @HaoWu 它有效,但它更像是一个拐杖
    • > a) 但是 Number.isFinite 已经明确定义了数字是可以传输的,但它不是保护函数(打字不同)。这就是为什么 TypeScript 不理解在那个分支中定义了“n”。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多