用户定义的类型保护功能和类型缩小到更具体的类型
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
,因为它可能是缺少 something
的 As<"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
允许多个品牌在同一对象上共存。
假设我有以下用户定义的类型保护函数,它检查一个值是否大于 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
,因为它可能是缺少 something
的 As<"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
允许多个品牌在同一对象上共存。