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

本文同步首发在个人博客中,欢迎订阅、交流。

用于条件判断时的 extends

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

  1. 若位于 extends 两侧的类型相同,则 extends 在语义上可理解为 ===,可以参考如下例子:
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false // false
  1. 若位于 extends 右侧的类型包含位于 extends 左侧的类型(即狭窄类型 extends 宽泛类型)时,结果为 true,反之为 false。可以参考如下例子:
type result3 = string extends string | number ? true : false // true
  1. 当 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 条件类型精读与实践的更多相关文章

  1. typescript进阶篇之高级类型与条件类型(Readonly, Partial, Pick, Record)

    本文所有东西尽可在 typescript 官网文档寻找,但是深浅不一 高级类型 lib 库中的五个高级类型 以下所有例子皆以 person 为例 interface Person { name: st ...

  2. 编写TypeScript工具类型,你需要知道的知识

    什么是工具类型 用 JavaScript 编写中大型程序是离不开 lodash 工具的,而用 TypeScript 编程同样离不开工具类型的帮助,工具类型就是类型版的 lodash .简单的来说,就是 ...

  3. TypeScript 中高级类型的理解?有哪些?

    一.是什么 除了string.number.boolean 这种基础类型外,在 typescript 类型声明中还存在一些高级的类型应用 这些高级类型,是typescript为了保证语言的灵活性,所使 ...

  4. TypeScript 高级类型

    ⒈交叉类型(Intersection Types) 交叉类型是将多个类型合并为一个类型. 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性. 例如, Person &a ...

  5. C# vs TypeScript - 高级类型

    总目录 从C#到TypeScript - 类型 从C#到TypeScript - 高级类型 从C#到TypeScript - 变量 从C#到TypeScript - 接口 从C#到TypeScript ...

  6. 从C#到TypeScript - 高级类型

    C# vs TypeScript - 高级类型 上一篇讲了基础类型,基本上用基础类型足够开发了,不过如果要更高效的开发,还是要看下高级类型,这篇和C#共同点并不多,只是延用这个主题. 联合类型 可以从 ...

  7. TypeScript 之类型判断

    在使用 Angular 做项目的时候,对 TypeScript 的类型判断不太熟练,为了方便查找,特意对 TypeScript 的类型判断做了简单梳理.文章只是 TS 官网的内容摘要,没有高深的知识, ...

  8. Typescript 开发环境的最佳实践

    Typescript 开发环境的最佳实践 0️⃣ git init(略) 1️⃣️️ 初始化:$ yarn add -D ts-node typescript 2️⃣ 生成 tsconfig.json ...

  9. TypeScript的类型

    ⒈TypeScript的类型 JavaScript语言的数据类型包括以下7种: 1.boolean(布尔),true || false 2.null,表明null值得特殊关键字,JavaScript是 ...

随机推荐

  1. Semaphore 类 的使用理解C#

    示例 下面的代码示例创建一个信号量,其最大计数为3,初始计数为零. 该示例启动五个线程,这会阻止等待信号量. 主线程使用 Release(Int32) 方法重载将信号量计数增加到其最大值,从而允许三个 ...

  2. MVVMLight学习笔记(一)---MVVMLight概述

    一.MVVM概述 MVVM是Model-View-ViewModel的简写,主要目的是为了解耦视图(View)和模型(Model). MVVM结构如下: 相对于之前把逻辑结构写在Code Behind ...

  3. ubuntu编译安装python3.6.5

    去官网下载安装包放到~/software/   下 1更新软件库 sudo apt-get update 2安装相应的开发库 sudo apt-get install zlib1g-dev libbz ...

  4. spring整合LOG4J2日志

    POM文件 <!-- Log Begin --> <slf4j.version>1.7.24</slf4j.version> <log4j2.version& ...

  5. mzy对于枚举的理解

    关于enum,其实就是简化了的class,功能就是提供一个个独立的.特定含义的常量! 在JDK5.0之前我们想模拟enum的功能,只能使用自定义类的形式: 1.首先私有化构造方法,让外部不能new对象 ...

  6. Fllink学习

    1.Apache Flink 是一个面向分布式数据流处理和批量数据处理的开源计算平台,它能够基于同一个Flink运行时,提供支持流处理和批处理两种类型应用的功能. 现有的开源计算方案,会把流处理和批处 ...

  7. 删除mysql数据库后django重建数据库

    问题:由于表的结构设计的不太合理,后来要添加列,但是在django中使用makemigrations一直失败. 解决:索性就把mysql中对于django的数据库删了(其实也不用删除),在django ...

  8. 小程序es6

    在小程序中使用ES6的新特性ECMAScript 6(简称ES6)是于2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015). 小程序在很久之前 ...

  9. Redis笔记(一)

    redis:1.什么是缓存? mybatis一级缓存和二级缓存 mybatis的一级缓存存在哪? SqlSession,就不会再走数据库 什么情况下一级缓存会失效? 当被更新,删除的时候sqlsess ...

  10. unexpected end of file while looking for precompiled header. Did you forget to add '#include "stdafx.h"' to your source 解决办法

    Project -> Properties -> C/C++ -> Precompiled Headers -> Precompiled Header -> 选择Not ...