TypeScript中条件类型精读与实践记录


Posted in Javascript onOctober 05, 2021

在大多数程序中,我们必须根据输入做出决策。TypeScript 也不例外,使用条件类型可以描述输入类型与输出类型之间的关系。

用于条件判断时的 extends

当 extends 用于表示条件判断时,可以总结出以下规律

若位于 extends 两侧的类型相同,则 extends 在语义上可理解为 ===,可以参考如下例子:

type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false     // false

若位于 extends 右侧的类型包含位于 extends 左侧的类型(即狭窄类型 extends 宽泛类型)时,结果为 true,反之为 false。可以参考如下例子:

type result3 = string extends string | number ? true : false // true

当 extends 作用于对象时,若在对象中指定的 key 越多,则其类型定义的范围越狭窄。可以参考如下例子:

type result4 = { a: true, b: false } extends { a: true } ? true : false // true

在泛型类型中使用条件类型

考虑如下 Demo 类型定义:

type Demo<T, U> = T extends U ? never : T

结合用于条件判断时的 extends,可知 'a' | 'b' | 'c' extends 'a' 是 false, 因此 Demo<'a' | 'b' | 'c', 'a'> 结果是 'a' | 'b' | 'c' 么?
查阅官网,其中有提到:

When conditional types act on a generic type, they become distributive when given a union type.

即当条件类型作用于泛型类型时,联合类型会被拆分使用。即 Demo<'a' | 'b' | 'c', 'a'> 会被拆分为 'a' extends 'a'、'b' extends 'a'、'c' extends 'a'。用伪代码表示类似于:

function Demo(T, U) {
  return T.map(val => {
    if (val !== U) return val
    return 'never'
  })
}

Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']

此外根据 never 类型的定义 —— never 类型可分配给每种类型,但是没有类型可以分配给 never(除了 never 本身)。即 never | 'b' | 'c' 等价于 'b' | 'c'。

因此 Demo<'a' | 'b' | 'c', 'a'> 的结果并不是 'a' | 'b' | 'c' 而是 'b' | 'c'。

工具类型

心细的读者可能已经发现了 Demo 类型的声明过程其实就是 TypeScript 官方提供的工具类型中 Exclude<Type, ExcludedUnion> 的实现原理,其用于将联合类型 ExcludedUnion 排除在 Type 类型之外。

type T = Demo<'a' | 'b' | 'c', 'a'> // T: 'b' | 'c'

基于 Demo 类型定义,进一步地还可以实现官方工具类型中的 Omit<Type, Keys>,其用于移除对象 Type
中满足 keys 类型的属性值。

type Omit<Type, Keys> = {
  [P in Demo<keyof Type, Keys>]: Type<P>
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type T = Omit<Todo, 'description'> // T: { title: string; completed: boolean }

逃离舱

如果想让 Demo<'a' | 'b' | 'c', 'a'> 的结果为 'a' | 'b' | 'c' 是否可以实现呢? 根据官网描述:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

如果不想遍历泛型中的每一个类型,可以用方括号将泛型给括起来以表示使用该泛型的整体部分。
type Demo<T, U> = [T] extends [U] ? never : T

type Demo<T, U> = [T] extends [U] ? never : T

// result 此时类型为 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>

在箭头函数中使用条件类型

在箭头函数中使用三元表达式时,从左向右的阅读习惯导致函数内容区若不加括号则会让使用方感到困惑。比如下方代码中 x 是函数类型还是布尔类型呢?

// The intent is not clear.
var x = a => 1 ? true : false

在 eslint 规则 no-confusing-arrow 中,推荐如下写法:

var x = a => (1 ? true : false)

在 TypeScript 的类型定义中,若在箭头函数中使用 extends 也是同理,由于从左向右的阅读习惯,也会导致阅读者对类型代码的执行顺序感到困惑。

type Curry<P extends any[], R> =
  (arg: Head<P>) => HasTail<P> extends true ? Curry<Tail<P>, R> : R

因此在箭头函数中使用 extends 建议加上括号,对于进行 code review 有很大的帮助。

type Curry<P extends any[], R> =
  (arg: Head<P>) => (HasTail<P> extends true ? Curry<Tail<P>, R> : R)

结合类型推导使用条件类型

在 TypeScript 中,一般会结合 extends 来使用类型推导 infer 语法。使用它可以实现自动推导类型的目的。比如用其来实现工具类型 ReturnType<Type> ,该工具类型用于返回函数 Type 的返回类型。
type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

MyReturnType<() => string>          // string
MyReturnType<() => Promise<boolean> // Promise<boolean>

结合 extends 与类型推导还可以实现与数组相关的 Pop<T>、Shift<T>、Reverse<T> 工具类型。

Pop<T>:

type Pop<T extends any[]> = T extends [...infer ExceptLast, any] ? ExceptLast : never

type T = Pop<[3, 2, 1]> // T: [3, 2]

Shift<T>:

type Shift<T extends any[]> = T extends [infer _, ...infer O] ? O : never

type T = Shift<[3, 2, 1]> // T: [2, 1]

Reverse<T>

type Reverse<T> = T extends [infer F, ...infer Others]
  ? [...Reverse<Others>, F]
  : []

type T = Reverse<['a', 'b']> // T: ['b', 'a']

使用条件类型来判断两个类型完全相等

我们也可以使用条件类型来判断 A、B 两个类型是否完全相等。当前社区上主要有两种方案:

方案一: 参考 issue

export type Equal1<T, S> =
  [T] extends [S] ? (
    [S] extends [T] ? true : false
  ) : false

目前该方案的唯一缺点是会将 any 类型与其它任何类型判为相等。

type T = Equal1<{x:any}, {x:number}> // T: true

方案二: 参考 issue

export type Equal2<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<U>() => U extends Y ? 1 : 2) ? true : false

目前该方案的唯一缺点是在对交叉类型的处理上有一点瑕疵。

type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // false

以上两种判断类型相等的方法见仁见智,笔者在此抛砖引玉。

总结

到此这篇关于TypeScript中条件类型精读与实践的文章就介绍到这了,更多相关TypeScript条件类型内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
使用Java实现简单的server/client回显功能的方法介绍
May 03 Javascript
JS操作iframe里的dom(实例讲解)
Jan 29 Javascript
javascript实现textarea中tab键的缩排处理方法
Jun 26 Javascript
jquery简单插件制作(fn.extend)完整实例
May 24 Javascript
Javascript实现倒计时(防页面刷新)实例
Dec 13 Javascript
关于Node.js中Buffer的一些你可能不知道的用法
Mar 28 Javascript
pace.js和NProgress.js两个加载进度插件的一点小总结
Jan 31 Javascript
解决vue-cli项目webpack打包后iconfont文件路径的问题
Sep 01 Javascript
nuxt中使用路由守卫的方法步骤
Jan 27 Javascript
vue.js实现会动的简历(包含底部导航功能,编辑功能)
Apr 08 Javascript
Flutter 超实用简单菜单弹出框 PopupMenuButton功能
Aug 06 Javascript
jQuery操作元素的内容和样式完整实例分析
Jan 10 jQuery
SSM VUE Axios详解
Ajax实现三级联动效果
Oct 05 #Javascript
5种 JavaScript 方式实现数组扁平化
Oct 05 #Javascript
国庆节到了,利用JS实现一个生成国庆风头像的小工具 详解实现过程
Oct 05 #Javascript
Javascript设计模式之原型模式详细
JS数组方法some、every和find的使用详情
8个JS的reduce使用实例和reduce操作方式
Oct 05 #Javascript
You might like
php数组函数序列之array_splice() - 在数组任意位置插入元素
2011/11/07 PHP
PHP两种去掉数组重复值的方法比较
2014/06/19 PHP
测试你的JS的掌握程度的代码
2009/12/09 Javascript
jquery1.4 教程二 ajax方法的改进
2010/02/25 Javascript
js判断子窗体是否关闭的方法
2015/08/11 Javascript
jQuery实现动态表单验证时文本框抖动效果完整实例
2015/08/21 Javascript
jQuery实现的超简单点赞效果实例分析
2015/12/31 Javascript
JavaScript中removeChild 方法开发示例代码
2016/08/15 Javascript
关于axios返回空对象的问题解决
2017/04/04 Javascript
js获取一组日期中最近连续的天数
2017/05/25 Javascript
如何获取TypeScript的声明文件.d.ts
2018/05/01 Javascript
jQuery实现的响应鼠标移动方向插件用法示例【附源码下载】
2018/08/28 jQuery
Vue.js 时间转换代码及时间戳转时间字符串
2018/10/16 Javascript
详解Vue+Element的动态表单,动态表格(后端发送配置,前端动态生成)
2019/04/20 Javascript
javascript异步编程的六种方式总结
2019/05/17 Javascript
原生js代码能实现call和bind吗
2019/07/31 Javascript
JS工厂模式开发实践案例分析
2019/10/17 Javascript
基于NodeJS开发钉钉回调接口实现AES-CBC加解密
2020/08/20 NodeJs
[01:00:12]2018DOTA2亚洲邀请赛 4.7 淘汰赛 VP vs LGD 第一场
2018/04/09 DOTA
使用python 获取进程pid号的方法
2014/03/10 Python
Python 3.x 新特性及10大变化
2015/06/12 Python
Python使用requests提交HTTP表单的方法
2018/12/26 Python
Pandas 重塑(stack)和轴向旋转(pivot)的实现
2019/07/22 Python
windows中安装Python3.8.0的实现方法
2019/11/19 Python
解决pycharm中导入自己写的.py函数出错问题
2020/02/12 Python
Python页面加载的等待方式总结
2021/02/28 Python
如何使用canvas绘制可移动网格的示例代码
2020/12/14 HTML / CSS
教育学专业毕业生的自我评价
2013/11/21 职场文书
劳动竞赛活动方案
2014/02/20 职场文书
政治表现评语
2014/05/04 职场文书
信用卡工资证明范本
2014/10/17 职场文书
年底个人总结范文
2015/03/10 职场文书
上班迟到检讨书
2015/05/06 职场文书
2016年圣诞节活动总结范文
2016/04/01 职场文书
mysql分组后合并显示一个字段的多条数据方式
2022/01/22 MySQL
Go语言实现一个简单的并发聊天室的项目实战
2022/03/18 Golang