前言
最初发现有这玩意是在styledcomponents的声明中,很神奇的写了个is。后来翻阅官方文档后发现,除了is是谓词签名外,还有assert断言签名。
官方文档
对于这种东西讲解,最好的方法就是先甩个官方文档。文档:https://www.typescriptlang.org/docs/handbook/release-notes/overview.html看一下官方给的例子:
function yell(str
) {
assert(typeof str
=== "string");
return str
.toUpperCase();
}
我们需要断言str是string ,如果str不是string,那么我们没法用大写的方法。比如我们要写这样一个函数:
function yell(str
:any) {
if (typeof str
!== "string") {
throw new TypeError("str should have been a string.");
}
return str
.toUpperCase();
}
正常来说,我们使用这种方法,ts会自动检测到后面走的str是个string,从而判断为string。如果我们想动态的判断条件呢?比如这么写:
function yell(str
:any) {
assert(typeof str
=== "string");
return str
.toUpperCase();
}
function assert(condition
: any) {
if (!condition
) {
throw new Error('错误');
}
}
由于不是一个函数里进行throw的,并且是进行判断,虽然ts没有报错,但是ts定的str是any而不是string。此时,就需要一种断言参数来进行断言:
function yell(str
:any) {
assert(typeof str
=== "string");
return str
.toUpperCase();
}
function assert(condition
: any): asserts condition
{
if (!condition
) {
throw new Error('错误');
}
}
对参数进行断言签名,此时str会被ts认定为string,而不是上面那个any。当然,在另一个函数中写throw可能让你觉得是throw帮助了ts。其实帮助ts的是assert,ts不会去检测另一个函数中的判断语句。比如这么写:
function yell(str
:any) {
console.log(str
)
assert(typeof str
=== "string");
return str
.toUpperCase();
}
function assert(condition
: any): asserts condition
{
return condition
}
我的assert函数并没有throw 别的类型,直接返回,但是由于asserts存在,下方的str依然变为string。除了asserts断言签名,还有种谓词签名,就是is了。is 和asserts有点像,但不一样。还是刚才例子,我们将asserts换成is:
function yell(str
:any) {
console.log(str
)
assert(typeof str
=== "string");
return str
.toUpperCase();
}
function assert(condition
: any):condition is
string {
return condition
}
会发现str仍然会被判断为any,而不是string。这是当然的,因为is不是asserts。is是谓词签名,所谓谓词签名,就是在另一个函数里强转参数,让使用其函数的函数可以正确判断类型。还是刚才例子,我们利用is来让str变成判断为string。
function yell(str
: any) {
console.log(str
);
if (assert(str
)) {
return str
.toUpperCase();
}
return str
.toUpperCase();
}
function assert(condition
: any): condition is
string {
return condition
;
}
可以看见,谓词签名其实就是断言签名的另一种表现形式,当我们断定谓词签名是string,另一个函数是的分支下则被检测为string。如果去了is,则判断仍是any。从中可以发现,ts不会检测另一个函数的判断语句,如果需要,分发判断,就要在另一个函数里做好参数签名,不管是谓词签名还是断言签名都可以。
实战使用
上面已经理解了asserts和is的用法,但是如果实战如何使用?对于asserts,其实上面的例子已经可以实战中使用了,动态断言类型而不用管断言函数内部使用。但是对于is,就没办法动态断言(当然结合泛型动态断言是另外用法,暂不讨论)。我们需要结合is的特点来分析。is的特点就是解决了函数在另一个函数中调用的参数判断,但是自己的参数判断就是正常的。基于这个特点,如果有个函数提供给别人使用为a类型,自己使用是b类型,这样就可以做出这种神奇的函数。那么什么样的函数会有上面一条特性?这种函数其实很多,几乎80%的复杂函数都会有这种特性!但是99%的人没这么写函数。我举个例子,首先写个接收各种数字的函数,如果数字等于1,2,3,4,5,6中一个则返回true。
function pipsAreValid(pips
: number) {
return (
pips
=== 1 ||
pips
=== 2 ||
pips
=== 3 ||
pips
=== 4 ||
pips
=== 5 ||
pips
=== 6
);
}
可以把这个类比成写个函数,然后经过各种逻辑,生成个判断。比如验证规则什么的过滤函数。当我把这个函数交给另一个函数使用时,我希望另一个函数经过我这个函数的判断,获得正确的类型推断:
type Dice
= 1 | 2 | 3 | 4 | 5 | 6;
function pipsAreValid(pips
: number):pips is Dice
{
return (
pips
=== 1 ||
pips
=== 2 ||
pips
=== 3 ||
pips
=== 4 ||
pips
=== 5 ||
pips
=== 6
);
}
function evalThrow(count
: number) {
if (pipsAreValid(count
)) {
console.log(count
)
}
}
这个参数断言有个特点,推断的再次判断会导致参数断言失效:
function evalThrow(count
: number) {
if (pipsAreValid(count
)===true) {
console.log(count
)
}
}
其实这个跟参数断言不关心返回值是一个道理,拿true与它相比,那么就说明计算的是返回值,而不是拿参数进行计算。但是这样是没问题的:
type Dice
= 1 | 2 | 3 | 4 | 5 | 6;
function pipsAreValid(pips
: number):pips is Dice
{
return (
pips
=== 1 ||
pips
=== 2 ||
pips
=== 3 ||
pips
=== 4 ||
pips
=== 5 ||
pips
=== 6
);
}
function demo2(pips
:number):pips is
8 | 9{
return(
pips
=== 8|| pips
===9
)
}
function evalThrow(count
: number) {
if (pipsAreValid(count
)||demo2(count
)) {
console.log(count
)
}
}
事实上虽然参数以冒号加断言语句覆盖了原来写返回值的地方,但是返回值仍然隐藏在签名中,仍会奏效。比如你不能这么写:
function evalThrow(count
: number) {
switch(count
){
case pipsAreValid(count
)://类型“
boolean”不可与类型“
number”进行比较
return ''
}
}
但是提供签名却是这样:
function pipsAreValid(pips
: number): pips is Dice
我还探究了一番,如果我写个声明文件:
xxx.d.ts
declare function newfn(pips
: number): pips is Dice
;
此时我进行调用:
function evalThrow(count
: number) {
switch(count
){
case newfn(count
)://类型“
boolean”不可与类型“
number”进行比较
return ''
}
}
也就是说,写了谓词签名的函数返回值固定为boolean。
同理,断言签名asserts固定返回值类型为void。
这样,一下子签名方面就明朗起来,使用带谓词签名或者断言签名,可以让我们更加优雅的替代as的强转,甚至做一些as也做不到的事情。