【问题标题】:User defined type guard function and type narrowing to more specific type用户定义的类型保护功能和类型缩小到更具体的类型
【发布时间】:2022-01-12 14:55:58
【问题描述】:

假设我有以下用户定义的类型保护函数,用于检查值是否为大于 1000 的数字:

function isBigNumber(something: unknown): something is number {
    return typeof something === "number" && something > 1000;
}

然后我这样使用它:

const strOrNum: string | number = "asdf";

if (isBigNumber(someVar)) {
    console.log(someVar * 10); // works because of type-guard
} else {
    // here type of strOrNum is "string" and no longer "string | number"
}

我的问题是 Else 块中 strOrNum 的类型。

看起来类型保护检查类型,并且 TS 也使用类型保护来缩小 Else 块的类型,在这种情况下,这不是我想要的。类型保护是否仅用于检查类型而没有传递给它们的值的附加信息?

在不将isBigNumber 的返回类型更改为boolean 并且不必再次检查strOrNum 的类型以获取If 语句的情况下,是否有解决方案?

【问题讨论】:

  • 为什么不将数字类型谓词与> 1000 条件分开?
  • @GuerricP 可能是一个解决方案。假设这种检查经常发生。那么使用这样的功能就有意义了。
  • 只是为了确认一下,您的实际情况是针对没有特定有限范围的大数?如果预期值足够小,可以使用一个可以工作的联合,否则我认为你需要进行大量令人讨厌的重构
  • @TadhgMcDonald-Jensen 这是我在项目中的实际用例的一个更简单的示例。看起来检查一个类型的子集,然后使用更广泛的类型作为类型保护的返回类型并不是一个好主意。
  • 特别是 number 在 typescript 中是原子的,将来可能会有数字范围允许这个用例,但现在这不是问题。如果您有类似 something is 1001 | 1002 | 1003 | 1004.... 的东西并且可以满足您的使用需求,它可能会起作用,但如果需要非整数或范围不是有限的,那么您无能为力。

标签: typescript typeguards


【解决方案1】:

没有>1000 类型。如果你在处理一个有限的、已知的数字集,你可以做类似something is 1001 | 1002 | 1003 | 1004 | 1005 的事情,Typescript 会跟踪它(并且会理解在false 的情况下something 仍然可以(其他一些)number),但你不是。

这里的解决方案是“类型标记”,这实际上是一种在 Typescript 的结构类型系统中“伪造”名义类型的方法。有很多方法,你可以安装几个库,但为此我将使用我自己的,我称之为As。我在这个答案的底部添加了As,以及一些解释,但也可以将其用作黑匣子。

As 的工作方式是你可以说某事是,比如说,As<"big-number">,Typescript 会尊重这一点,并跟踪这个很大的数字。它纯粹存在于类型系统中,并从编译的 Javascript 中完全消失。有了它,你可以编写只接受大数字的函数,你可以编写确认大数字的类型保护等等。

关于这些品牌的一个真正关键的事情是,如果你有一个显示 something is X & As<"whatever"> 的 typeguard,Typescript 会理解 something 可能仍然是 X,因为它可能是 As<"whatever"> 部分987654334@ 丢失。这可以解决您的 typeguard 问题。

所以,对于你的例子:

type BigNumber = number & As<"big-number">;

function isBigNumber(something: unknown): something is BigNumber {
    return typeof something === "number" && something > 1000;
}

function onBigNumber(value: BigNumber): void {
  console.log(value - 1000); // works because BigNumber extends number
}

declare const strOrNum: string | number;

if (isBigNumber(strOrNum)) {
  console.log(strOrNum * 10); // works because of type-guard
  onBigNumber(strOrNum); // works because of type-guard
} else {
  // here type of strOrNum is "string | number"
  if (typeof strOrNum === "number") {
    console.log(strOrNum * 100); // works because of type-guard
    onBigNumber(strOrNum); // ERROR, because strOrNum is number but not As<"big-number">
  }
}

As的定义

declare abstract class As<Tag extends keyof never> {
  private static readonly $as$: unique symbol;
  private [As.$as$]: Record<Tag, true>;
}

如果您想理解这一点而不是仅仅将其视为黑匣子,请注意一些要点:

  • declare 表示 TS 不会为这个类生成代码——它告诉 TS 这个类的运行时 JS 已经存在,我们只是通知 TS 它的存在。这是一个谎言,在这种情况下——这个类根本不存在。
  • abstract 阻止我们尝试 new As,由于上面讨论的“谎言”,这将导致运行时错误。它不会阻止我们尝试class X extends As,它会编译,也会导致运行时错误。不要那样做。 (不过,如果你想扩大谎言,你可以declare class X extends As。)
  • Tag extends keyof never 使 As 通用,因此我们可以将它与许多不同的品牌一起使用。 keyof never 是确定与合法 JS 对象键 (string | number | symbol) 对应的类型联合的奇怪最佳实践。
  • private [As.$as$] 告诉 Typescript 这个类的对象有一个私有成员,也就是说,它的结构是不同的和特定的。这意味着 Typescript 的结构类型系统会将其视为不同的类型。它是private,所以没有人可以访问它(这很好,因为这是另一个谎言),并且使用定义为unique symbolAs.$as$,保证我们不会与我们应用的任何名称冲突这个品牌。
  • Record&lt;Tag, true&gt; 作为私有成员的类型“存储”Tag 传递到类的通用参数中的任何内容。将其设为Record 允许多个品牌在同一个对象上共存。

【讨论】:

  • 这很有趣。为什么需要Record?您传递给As 的每个不同类型参数不是都会创建不同的类类型吗?
  • @KrisztiánBalla 因为 Typescript 是一个结构化类型系统,所以任何类都类型等价于具有相同成员的任何其他对象。所以我们必须告诉 TS 它实际上有一些基于 Tag 的成员,以使其识别出一个 As 不等同于其他一些 As(或一个空对象,在 As 定义的情况下根本没有任何成员)。
  • 感谢您的解释。我总是忘记 TS 的结构类型。我会试试这个,看看它是如何工作的。
  • 我已经试过了,效果很好。请注意:我将抽象类放在 declare global 块中,以便它在代码库中的任何地方都可用。
猜你喜欢
  • 2019-01-03
  • 1970-01-01
  • 2021-10-31
  • 2020-11-11
  • 2015-12-19
  • 2019-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多