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是 ...
随机推荐
- Python中的reduce()函数
reduce()函数也是Python内置的一个高阶函数.reduce()函数接收的参数和 map()类似,一个函数 f,一个list,但行为和 map()不同,reduce()传入的函数 f 必须接收 ...
- web项目中的浏览器行为和服务器行为
package day10.think_about_path; import java.io.IOException; import javax.servlet.ServletException; i ...
- 常用数据库JDBC
JDBC的URL=协议名+子协议名+数据源名.1. 协议名总是"jdbc".2.子协议名由JDBC驱动程序的编写者决定.3. 数据源名也可能包含用户与口令等信息:这些信息也可单独提 ...
- RabitMq过期时间TTL
第一种:给消息设置过期时间 启动一个插件 @Bean public DirectExchange DirectExchange() { return new DirectExchange(" ...
- mybaits源码分析--日志模块(四)
一.日志模块 首先日志在我们开发过程中占据了一个非常重要的地位,是开发和运维管理之间的桥梁,在Java中的日志框架也非常多,Log4j,Log4j2,Apache Commons Log,java.u ...
- Win7/Win10+VS2017+OpenCV3.4.2安装、测试
安装VS2017 在微软官网https://www.microsoft.com,下载Visual Studio 2017安装包 用管理员权限运行vs2017 enterprise安装包,安装过程会持续 ...
- x和y为正整数变量,求满足 x+y | xy 的通解。
x和y为正整数变量,求满足 x+y | xy 的通解. 解:由题设可知存在正整数t满足t(x+y)=xy. 设m=(x,y),则存在正整数u和v满足: x=mu, y=mv, (u,v)=1. 于是有 ...
- Electron团队为什么要干掉remote模块
Electron团队提供remote模块给开发者, 主要目的是为了简化渲染进程和主进程互访的难度, 这个目的却是达到了. 但也带来了很多问题, 归纳起来主要分为以下四点: 第一:它很慢 通过remot ...
- 手撕LRU缓存
面试官:来了,老弟,LRU缓存实现一下? 我:直接LinkedHashMap就好了. 面试官:不要用现有的实现,自己实现一个. 我:..... 面试官:回去等消息吧.... 大家好,我是程序员学长,今 ...
- Pulsar の 保证消息的顺序性、幂等性和可靠性
原文链接:Pulsar の 保证消息的顺序性.幂等性和可靠性 一.背景 前面两篇文章,已经介绍了关于Pulsar消费者的详细使用和自研的Pulsar组件. 接下来,将简单分析如何保证消息的顺序性.幂等 ...