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

Jen*_*lly 3 typescript typeguards

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

function isBigNumber(something: unknown): something is number {
    return typeof something === "number" && something > 1000;
}
Run Code Online (Sandbox Code Playgroud)

然后我这样使用它:

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"
}
Run Code Online (Sandbox Code Playgroud)

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

看起来类型防护检查类型,并且 TS 还使用类型防护来缩小 Else 块的类型范围,在本例中这不是我想要的。类型防护是否仅用于检查类型,而没有传递给它们的值的附加信息?

isBigNumber是否有解决方案,无需更改to的返回类型并且不必再次检查 If 语句的boolean类型?strOrNum

KRy*_*yan 9

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

\n

这里的解决方案是 \xe2\x80\x9ctype 品牌,\xe2\x80\x9d 这实际上是在 Typescript\xe2\x80\x99s 结构类型系统中 \xe2\x80\x9cfake\xe2\x80\x9d 名义键入的一种方法。您可以安装多种方法和多个库,但为此我\xe2\x80\x99m 将使用我自己的,我只是将其称为As. 我\xe2\x80\x99ve包含As在这个答案的底部,并附有一些解释,但它\xe2\x80\x99s也可以将其用作黑匣子。

\n

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

\n

关于这些品牌真正重要的事情之一是,如果你有一个 typeguard 说something is X & As<"whatever">,Typescript 会理解它something可能仍然是X,因为它可能是缺少的As<"whatever">部分something。这解决了你的打字保护问题。

\n

所以,对于你的例子:

\n
type BigNumber = number & As<"big-number">;\n\nfunction isBigNumber(something: unknown): something is BigNumber {\n    return typeof something === "number" && something > 1000;\n}\n\nfunction onBigNumber(value: BigNumber): void {\n  console.log(value - 1000); // works because BigNumber extends number\n}\n\ndeclare const strOrNum: string | number;\n\nif (isBigNumber(strOrNum)) {\n  console.log(strOrNum * 10); // works because of type-guard\n  onBigNumber(strOrNum); // works because of type-guard\n} else {\n  // here type of strOrNum is "string | number"\n  if (typeof strOrNum === "number") {\n    console.log(strOrNum * 100); // works because of type-guard\n    onBigNumber(strOrNum); // ERROR, because strOrNum is number but not As<"big-number">\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

的定义As

\n
declare abstract class As<Tag extends keyof never> {\n  private static readonly $as$: unique symbol;\n  private [As.$as$]: Record<Tag, true>;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
    \n
  • declare意味着TS不会\xe2\x80\x99t生成这个类的代码\xe2\x80\x94它告诉TS这个类的运行时JS已经存在,我们\xe2\x80\x99只是通知TS它的存在。那\xe2\x80\x99是一个谎言,在这种情况下\xe2\x80\x94这个类根本不存在\xe2\x80\x99。
  • \n
  • abstract阻止我们尝试new As,这会由于上面讨论的 \xe2\x80\x9clie\xe2\x80\x9d 而导致运行时错误。它不会阻止我们尝试编译class X extends As,并且还会导致运行时错误。不要\xe2\x80\x99 这样做。(declare class X extends As不过,如果你想扩大谎言,你可以。)
  • \n
  • Tag extends keyof never是As通用的,所以我们可以将它与许多不同的品牌一起使用。keyof never是确定与合法 JS 对象键 ( ) 对应的类型联合的奇怪最佳实践string | number | symbol。
  • \n
  • private [As.$as$]告诉 Typescript 该类的对象有一个私有成员,也就是说,它的结构是不同的且特定的。这意味着 Typescript\xe2\x80\x99s 结构类型系统会将其视为不同的类型。它\xe2\x80\x99s private,所以没有人可以访问它(这很好,因为这是另一个谎言),并且使用As.$as$,它被定义为 a unique symbol,保证我们不会\xe2\x80\x99t 与我们的任何名称发生冲突。应用这个品牌。
  • \n
  • Record<Tag, true>作为私有成员 \xe2\x80\x9cstores\xe2\x80\x9d 的类型,无论Tag传递给类\xe2\x80\x99s 泛型参数。使其成为Record允许多个品牌在同一对象上共存的方式。
  • \n
\n