用户定义的类型保护功能和类型缩小到更具体的类型

User defined type guard function and type narrowing to more specific type

假设我有以下用户定义的类型保护函数,它检查一个值是否大于 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 的 return 类型更改为 boolean 并且必须再次检查 If 语句的 strOrNum 类型?

没有>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"> 的类型保护,Typescript 会理解 something 可能仍然是 X,因为它可能是缺少 somethingAs<"whatever"> 部分。这解决了你的类型保护问题。

因此,对于您的示例:

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不会为这个class生成代码——它告诉TS这个class的runtime JS已经存在,我们只是通知TS它的存在。在这种情况下,这是一个谎言——这个 class 根本不存在。
  • 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 这个 class 的对象有一个私有成员,即它的结构是不同的和特定的。这意味着 Typescript 的结构类型系统会将其视为不同的类型。它是 private,所以没有人可以访问它(这很好,因为这是另一个谎言),并且使用 As.$as$,它被定义为 unique symbol,保证我们不会有任何名称与我们应用此品牌的任何内容冲突。
  • Record<Tag, true> 作为私有成员的类型“存储”任何 Tag 传递给 class 的通用参数。使其成为 Record 允许多个品牌在同一对象上共存。