TypeScript 条件类型精读与实践
在大多数程序中,我们必须根据输入做出决策。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
// 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
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进阶篇之高级类型与条件类型(Readonly, Partial, Pick, Record)
本文所有东西尽可在 typescript 官网文档寻找,但是深浅不一 高级类型 lib 库中的五个高级类型 以下所有例子皆以 person 为例 interface Person { name: st ...
- 编写TypeScript工具类型,你需要知道的知识
什么是工具类型 用 JavaScript 编写中大型程序是离不开 lodash 工具的,而用 TypeScript 编程同样离不开工具类型的帮助,工具类型就是类型版的 lodash .简单的来说,就是 ...
- TypeScript 中高级类型的理解?有哪些?
一.是什么 除了string.number.boolean 这种基础类型外,在 typescript 类型声明中还存在一些高级的类型应用 这些高级类型,是typescript为了保证语言的灵活性,所使 ...
- TypeScript 高级类型
⒈交叉类型(Intersection Types) 交叉类型是将多个类型合并为一个类型. 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性. 例如, Person &a ...
- C# vs TypeScript - 高级类型
总目录 从C#到TypeScript - 类型 从C#到TypeScript - 高级类型 从C#到TypeScript - 变量 从C#到TypeScript - 接口 从C#到TypeScript ...
- 从C#到TypeScript - 高级类型
C# vs TypeScript - 高级类型 上一篇讲了基础类型,基本上用基础类型足够开发了,不过如果要更高效的开发,还是要看下高级类型,这篇和C#共同点并不多,只是延用这个主题. 联合类型 可以从 ...
- TypeScript 之类型判断
在使用 Angular 做项目的时候,对 TypeScript 的类型判断不太熟练,为了方便查找,特意对 TypeScript 的类型判断做了简单梳理.文章只是 TS 官网的内容摘要,没有高深的知识, ...
- Typescript 开发环境的最佳实践
Typescript 开发环境的最佳实践 0️⃣ git init(略) 1️⃣️️ 初始化:$ yarn add -D ts-node typescript 2️⃣ 生成 tsconfig.json ...
- TypeScript的类型
⒈TypeScript的类型 JavaScript语言的数据类型包括以下7种: 1.boolean(布尔),true || false 2.null,表明null值得特殊关键字,JavaScript是 ...
随机推荐
- Javascript - Vue - 动画
动画状态类名 vue动画通过将需要执行动画的标签放入transition标签中,再通过设置预置的vue动画类名的css样式来控制动画的呈现效果. 开场动画状态的三个类名 v-enter:动画开始之前的 ...
- io中的特殊流Properties
对于去年学习IO的时候一些代码贴上来: 初识properties,因为继承自hashtable,其中可以使用put操作: package special; import java.util.Prope ...
- 工具库用久了,你还会原生操作 Cookie 吗?
用得好了,工具库和框架确实是一大助力,但就怕我们会因此习惯了走捷径,而忘了自己的根本依靠是什么. 前言 前端技术的飞速发展,给从业人员不可避免地带来了"疲劳"感,我们常常会感叹学不 ...
- 25道经典Java算法题
题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? //这是一个菲波拉契数列问题 [Java] 纯 ...
- OpenCV 生成矩形mask
生成mask的一种操作 cv::Mat mask = cv::Mat::zeros(300, 300, CV_8UC1); mask(cv::Rect(100,150,100, 50)) = 255; ...
- 用C++实现的增强Eratosthenes筛法程序
运行示例 PS H:\Read\num\x64\Release> .\eSievePro Eratosthenes sieve: a method to find out all primes ...
- MySQL列举常见的关系型数据库和非关系型都有那些?
关系型数据库: Oracle.DB2.Microsoft SQL Server.Microsoft Access.MySQL 非关系型数据库: NoSql.Cloudant.MongoDb.redis ...
- SQL查询语句执行流程
msyql执行流程 你有个最简单的表,表里只有一个 ID 字段,在执行下面这个查询语句时:: select * from T where ID=10: 我们看到的只是输入一条语句,返回一个结果,却不知 ...
- GIMP 一键均匀添加多条参考线 一键均匀切分图片
添加参考线 #!/usr/bin/env python2 # -*- coding: utf-8 -*- from gimpfu import * # orientation: ORIENTATION ...
- Aggressor Script 开发-Powershell 免杀
转载https://www.jianshu.com/p/f158a9d6bdcf 前言 在接触到Cobalt Strike的时候就知道有各种插件,想象着那天也可以自己学习编写一个.在之前分析Cobal ...