你可能并没有真正理解 TS 中的 never 类型
我相信很多人看到这个 never 类型的时候都会有种疑惑,这个类型有什么用?
先来看看 TypeScript
中的定义:“返回 never
的函数必须存在无法达到的终点”。啥???好像解释了什么又好像什么都没说。既然无法到达终点,那返回这个 never
的类型,能用来干嘛呢?
1、使联合类型更安全
我知道你很急,但是你先别急,先来看个例子:
type MethodType = 'GET' | 'POST';
function request(method: MethodType) {
switch(method){
case 'GET':
return '使用 GET 方法获取到的数据';
case 'POST':
return '使用 POST 方法获取到的数据';
}
}
上述代码中,首先定义了一个 MethodType
联合类型,用于约束 request
方法中的 method
参数必须为 GET
或 POST
,然后在 request
方法中通过 switch
分支判断传入的请求参数并返回对应的数据。好,到这里代码都没什么问题,能正常执行。
可能有些细心的小伙伴可能发现了:“你这里是不是少了个 default
分支?”其实呢,在这里加不加 default
分支都行,因为已经有 MethodType
类型约束了参数的类型只能是 GET
或 POST
,按照这个类型逻辑来说是不可能进入 default
分支的。但是编程规范又告诉我们:“switch 语句中应当含有 default 分支”,好,那咱们就加上吧。于是,代码变成了下面这样:
type MethodType = 'GET' | 'POST';
function request(method: MethodType) {
switch(method){
case 'GET':
return '使用 GET 方法获取到的数据';
case 'POST':
return '使用 POST 方法获取到的数据';
default:
return '返回默认数据';
}
}
然后又有小伙伴要问了,你这跟 never
有什么关系呢?你先忍一忍,接着往下看,我们分别在这三个 case
分支中看看 method
的类型是什么:
- GET 分支:
- POST 分支:
- default 分支:
可以看到,不同分支中 method
所对应的类型是不一致的。这得益于 TS
中的类型收窄功能,它会分析你的分支,根据分支中不同的条件来对类型做一个相应的收窄。例如上述示例中的 GET
分支,method
就被收窄成 GET
类型了,不再是 MethodType
这个联合类型了,其他分支同理。
接下来,重点讲讲 default
分支为啥是 never
类型。前面咱们也分析过按照正常逻辑是不可能进入 default
分支的,咋办? TS
总得搞个什么东西表示一下吧,好了,这个东西就是今天的主角 never
,这也印证了前面开头的那句话 “返回 never
的函数必须存在无法达到的终点”,对应到这个例子中,request
函数无法达到的终点就是 default
分支。怎么样,是不是有一种豁然开朗的感觉!
到这里咱们已经理解了 never
所表达的意思,那它有什么具体用处呢?接着往下看。
前面不是说过 default
分支没啥用吗,那我们真就把它删了可以吗?目前是没啥问题,那假如后续由于需求的变更,需要对请求类型进行扩展,得加一个 DELETE
类型,代码如下:
type MethodType = 'GET' | 'POST' | 'DELETE';
function request(method: MethodType) {
switch(method){
case 'GET':
return '使用 GET 方法获取到的数据';
case 'POST':
return '使用 POST 方法获取到的数据';
}
}
这不就出问题了吗?request
函数并没有处理 DELETE
分支。当然,这里由于示例代码比较简单,你一眼就看出来了需要在 switch
语句中加上这个 DELETE
分支的处理逻辑。
但是,一般来说,使用 TS
的项目都是一些比较大型的项目,此时你并不知道加了这个 DELETE
类型会影响哪些地方,也不知道要修改哪些函数,而且很难一一找到使用这个类型的地方,关键是编译器也不会报错。这时候,bug 不就来了吗?那怎么避免这种情况的出现呢?嘿,never
就可以派上用场了!
咱们稍微改造一下前面的 default
分支:
type MethodType = 'GET' | 'POST';
function request(method: MethodType) {
switch (method) {
case 'GET':
return '使用 GET 方法获取到的数据';
case 'POST':
return '使用 POST 方法获取到的数据';
default:
const m: never = method;
return m + '返回默认数据';
}
}
主要添加的逻辑是:在 default 分支中新建一个 never 类型的变量 m,并将 method 赋值给它,然后通过 return 返回出去
。其实这里并不用关心返回的是啥,因为执行不到这个分支。可以看到此时代码并没啥问题,m 变量的类型与 method 的类型是匹配的,所以编译器也不报错。
好了,重点来了!!!下面咱们把新需求中的 DELETE 类型给加上去:
type MethodType = 'GET' | 'POST' | 'DELETE';
function request(method: MethodType) {
switch (method) {
case 'GET':
return '使用 GET 方法获取到的数据';
case 'POST':
return '使用 POST 方法获取到的数据';
default:
const m: never = method;
return m + '返回默认数据';
}
}
哦吼,编译器报错了,如下图:
为啥呢?因为根据 TS
的类型收窄功能,它知道在这个分支里面还有一种情况,那就是 method
的类型为 DELETE
时候。它是可达的,所以不能赋值给 never
,导致编译器报错了。
这个时候咱们就可以在编码阶段非常清楚的知道,这个函数里面应该添加一个 DELETE
分支。假设引用这个类型的函数特别多,咱们也可以根据报错信息一一的进行修改,如果不修改,项目就运行不起来。这样的话,再也不用担心漏改导致发布之后出 bug 了。
怎么样,never
的用处是不是也理解了。那咱们再看个例子,了解下 never
的另一个用处。
2、排除特定的类型
假如你要定义一个这样的函数:该函数的参数不能为数字类型,但可以是其他的任何类型。意思就是得排除数字类型的参数。
咱们尝试的实现一下:
function call(param) {
if (typeof param === 'number') {
throw new Error('参数不能是数字类型')
}
return param
}
首先,这个函数功能是没有任何问题的。但是这个函数发生错误的时间被延迟到了运行时,也就意味着在编写代码的时候,收不到任何的错误提示。
那能不能利用 TS
的类型约束功能将发生错误的时间提升到编译阶段呢?答案是肯定的。
咱们来改造一下这个 call
函数:
function call<T>(param: T extends number ? never : T) {
return param;
}
经过改造之后,这个函数将接收一个泛型 T,再给参数 param 的类型加了个条件判断:如果接收到的泛型 T 是 number 类型就给它赋值为 never 类型,否则直接使用这个泛型 T
,这样一来就达到目的了。
当咱们给它传一个 number
类型参数的时候,由于类型约束的存在,T extends number
这个条件被满足,于是 param 参数就变成了一个 never
类型,而 never
类型是不能接收 number
类型参数的。此时,编译器就报错了。
而且传其他任何类型都是没有任何问题的:
咱们也可以让代码更优雅一点,把参数类型提出来单独定义:
// 排除数字类型
type ExcludeNumberType<T> = T extends number ? never : T;
function call<T>(param: ExcludeNumberType<T>) {
return param;
}
或者让这个类型更通用一点,可以排除任何类型:
// 排除任何类型
type ExcludeType<T, E> = T extends E ? never : T;
function call<T>(param: ExcludeType<T, number>) {
return param;
}
总结
通过本篇文章,咱们理解了 never
类型所表达的意思:返回 never 的函数必须存在无法达到的终点。
以及它的两个实际用处:
- 使联合类型更安全
- 排除特定的类型
都看到这了,不妨来个一键三连(点赞 + 关注 + 收藏)?
欢迎大家在评论区留下宝贵的建议!
作者:HashTang
转载自:https://juejin.cn/post/7194232537063882811