深入理解 TypeScript 高级用法

核心提示前言:这里的标题看起来是 "高级用法",不少同学可能就表示被劝退了。其实 TypeScript 作为一门 强类型 编程语言,最具特色的就是他的类型表达能力,这是很多完备的后端语言都难以媲美的 ~~说的很对,但PHP是最好的语言~~,所以如果

前言:这里的标题看起来是 "高级用法",不少同学可能就表示被劝退了。其实 Typescript 作为一门 强类型 编程语言,最具特色的就是他的类型表达能力,这是很多完备的后端语言都难以媲美的 ~~说的很对,但PHP是最好的语言~~,所以如果你搞懂了他的类型系统,对将来的日常开发一定是大有裨益的,但过于灵活的类型系统也注定了 Typescript 无法成为一门纯粹的静态语言,不过每一行代码都有代码提示他不香嘛?
大纲
  • 基础准备
  • Typescript 类型系统简述
    • Typescript 的类型是支持定义 "函数定义" 的
    • Typescript 的类型是支持 "条件判断" 的
    • Typescript 的类型是支持 "数据结构" 的
    • Typescript 的类型是支持 "作用域" 的
    • Typescript 的类型是支持 "递归" 的
  • "高级用法" 的使用场景与价值
    • 哪些用法可以被称为 "高级用法"
    • 举例说明 "高级用法" 的使用场景
  • 类型推导与泛型操作符
    • 流动的类型(类型编写思路)
    • Typescript 代码哲学
  • 常见类型推导实现逻辑梳理与实践入门
    • 类型的传递(流动)
    • 类型的过滤与分流
  • Typescript 类型体操指北
  • 定制化扩展你的 Typescript
    • Typescript Service Plugins 的产生背景、功能定位、基础使用
    • 市面上已有的 Typescript Service Plugins 举例介绍
    • 参考资料链接
  • Q&A(欢迎评论区补充)
    • 可以利用 Typescript Service Plugin(例如配置 eslint 规则)阻塞编译或者在编译时告警吗?
基础准备预备知识本文的定位为理解高级用法,故不会涉及过多基础知识相关的讲解,需要读者自己去完善这方面的知识储备。

此文档的内容默认要求读者已经具备以下知识:

  1. Javascript 或其他语言编程经验。

  2. Typescript 实际使用经验,最好在正经项目中完整地使用过。
  3. 了解 Typescript 基础语法以及常见关键字地作用。
  4. Typescript类型系统 架构有一个最基本的了解。

相关资源推荐
  1. Typescript 官网
  2. Typescript Deep Dive
  3. Typescript GitHub地址
背景初用 Typescript 开发的同学一定有这样的困扰:
  1. 代码代码提示并不智能,似乎只能显式的定义类型,才能有代码提示,无法理解这样的编程语言居然有这么多人趋之若鹜。

  2. 各种各样的类型报错苦不堪言,本以为听信网上说 Typescript 可以提高代码可维护性,结果却发现徒增了不少开发负担。
  3. 显式地定义所有的类型似乎能应付大部分常见,但遇到有些复杂的情况却发现无能为力,只能含恨写下若干的 as any 默默等待代码 review 时的公开处刑。
  4. 项目急时间紧却发现 Typescript 成了首要难题,思索片刻决定投靠的 Anyscript,快速开发业务逻辑,待到春暖花开时再回来补充类型。

    双倍的工作量,双倍的快乐只有自己才懂。

为了避免以上悲剧的发生或者重演,我们只有在对它有更加深刻的理解之后,才能在开发时游刃有余、在撸码时纵横捭阖。

Typescript 类型系统简述
思考题:有人说 Typescript = Type + Javascript,那么抛开 Javascript 不谈,这里的 Type 是一门完备的编程语言吗?
Typescript 的类型是支持定义 "函数定义" 的有过编程经验的同学都知道,函数是一门编程语言中最基础的功能之一,函数是过程化、面向对象、函数式编程中程序封装的基本单元,其重要程度不言而喻。

函数可以帮助我们做很多事,比如 :

  • 函数可以把程序封装成一个个功能,并形成函数内部的变量作用域,通过静态变量保存函数状态,通过返回值返回结果。
  • 函数可以帮助我们实现过程的复用,如果一段逻辑可以被使用多次,就封装成函数,被其它过程多次调用。
  • 函数也可以帮我们更好地组织代码结构,帮助我们更好地维护代码。

那么言归正传,如何在 Typescript 类型系统中定义函数呢?Typescript 中类型系统中的的函数被称作 泛型操作符,其定义的简单的方式就是使用 type 关键字:// 这里我们就定义了一个最简单的泛型操作符type foo = T;这里的代码如何理解呢,其实这里我把代码转换成大家最熟悉的 Javascript 代码其实就不难理解了:

// 把上面的类型代码转换成 `Javascript` 代码

function

foo

{

return

T

}

那么看到这里有同学心里要犯嘀咕了,心想你这不是忽悠我嘛?这不就是 Typescript 中定义类型的方式嘛?这玩意儿我可太熟了,这玩意儿不就和 interface 一样的嘛,我还知道 Type 关键字和 interface 关键字有啥细微的区别呢!嗯,同学你说的太对了,不过你不要着急,接着听我说,其实类型系统中的函数还支持对入参的约束。

// 这里我们就对入参 T 进行了类型约束

type

foo

<

T

extends

string

>

=

T

;

那么把这里的代码转换成我们常见的 Typescript 是什么样子的呢?function foo {return T}当然啦我们也可以给它设置默认值:// 这里我们就对入参 T 增加了默认值type foo = T;那么这里的代码转换成我们常见的 Typescript 就是这样的:function foo {return T}看到这里肯定有同学迫不及待地想要提问了:那能不能像 JS 里的函数一样支持剩余参数呢?很遗憾,目前暂时是不支持的,但是在我们日常开发中一定是有这样的需求存在的。那就真的没有办法了嘛?其实也不一定,我们可以通过一些骚操作来模拟这种场景,当然这个是后话了,这里就不作拓展了。Typescript 的类型是支持 "条件判断" 的
人生总会面临很多选择,编程也是一样。

——我瞎编的

条件判断也是编程语言中最基础的功能之一,也是我们日常撸码过程成最常用的功能,无论是 if else 还是 三元运算符,相信大家都有使用过。那么在 Typescript 类型系统中的类型判断要怎么实现呢?其实这在 Typescript 官方文档被称为 条件类型,定义的方法也非常简单,就是使用 extends 关键字。T extends U X : Y;这里相信聪明的你一眼就看出来了,这不就是 三元运算符 嘛!是的,而且这和三元运算符的也发也非常像,如果 T extends Utrue 那么 返回 X ,否则返回 Y。结合之前刚刚讲过的 "函数",我们就可以简单的拓展一下:type num = 1;type str = 'hello world';type IsNumber = N extends number 'yes, is a number' : 'no, not a number';type result1 = IsNumber; // "yes, is a number"type result2 = IsNumber; // "no, not a number"这里我们就实现了一个简单的带判断逻辑的函数。

Typescript 的类型是支持 "数据结构" 的模拟真实数组看到这里肯定有同学就笑了,这还不简单,就举例来说,Typescript 中最常见数据类型就是 数组(Array) 或者 元组(tuple)。同学你说的很对,那你知道如何对 元组类型pushpopshiftunshift 这些行为操作吗?其实这些操作都是可以被实现的:// 这里定义一个工具类型,简化代码type ReplacevalByOwnKey = { [P in keyof T]: S[P] };// shift actiontype ShiftAction = => any) extends => any) R : never;// unshift actiontype UnshiftAction = => any) extends => any) R : never;// pop actiontype PopAction = ReplacevalByOwnKey, T>;// push actiontype PushAction = ReplacevalByOwnKey, T & { [k: string]: E }>;// test ...type tuple = ['vue', 'react', 'angular'];type resultWithShiftAction = ShiftAction; // ["react", "angular"]type resultWithUnshiftAction = UnshiftAction; // ["jquery", "vue", "react", "angular"]type resultWithPopAction = PopAction; // ["vue", "react"]type resultWithPushAction = PushAction; // ["vue", "react", "angular", "jquery"]

注意:这里的代码仅用于测试,操作某些复杂类型可能会报错,需要做进一步兼容处理,这里简化了相关代码,请勿用于生产环境!
相信读到这里,大部分同学应该可以已经可以感受到 Typescript 类型系统的强大之处了,其实这里还是继续完善,为元组增加 concatmap 等数组的常用的功能,这里不作详细探讨,留给同学们自己课后尝试吧。但是其实上面提到的 "数据类型" 并不是我这里想讲解的 "数据类型",上述的数据类型本质上还是服务于代码逻辑的数据类型,其实并不是服务于 类型系统 本身的数据类型。上面这句话的怎么理解呢?不管是 数组 还是 元组,在广义的理解中,其实都是用来对 数据批量操作,同理,服务于 类型系统 本身的数据结构,应该也可以对 类型批量操作

那么如何对 类型批量操作 呢?或者说服务于 类型系统 中的 数组 是什么呢?下面就引出了本小节真正的 "数组":联合类型说起 联合类型 ,相信使用过 Typescript 同学的一定对它又爱又恨:

  1. 定义函数入参的时候,当同一个位置的参数允许传入多种参数类型,使用 联合类型 会非常的方便,但想智能地推导出返回值的类型地时候却又犯了难。
  2. 当函数入参个数不确定地时候,又不愿意写出 => void 这种毫无卵用的参数类型定义。
  3. 使用 联合类型 时,虽然有 类型守卫(Type guard),但是某些场景下依然不够好用。

其实当你对它有足够的了解时,你就会发现 联合类型交叉类型 不知道高到哪里去了,~~我和它谈笑风生~~。类型系统中的 "数组"下面就让我们更加深入地了解一下 联合类型:如何遍历 联合类型 呢?既然目标是 批量操作类型,自然少不了类型的 遍历,和大多数编程语言方法一样,在 Typescript 类型系统中也是 in 关键字来遍历。type key = 'vue' | 'react';type MappedType = { [k in key]: string } // { vue: string; react: string; }你看,通过 in 关键字,我们可以很容易地遍历 联合类型,并对类型作一些变换操作。

但有时候并不是所有所有 联合类型 都是我们显式地定义出来的。我们想动态地推导出 联合类型 类型有哪些方法呢?可以使用 keyof 关键字动态地取出某个键值对类型的 keyinterface Student { name: string; age: number;}type studentKey = keyof Student; // "name" | "age"同样的我们也可以通过一些方法取出 元组类型 子类型type framework = ['vue', 'react', 'angular'];type frameworkVal1 = framework[number]; // "vue" | "react" | "angular"type frameworkVal2 = framework[any]; // "vue" | "react" | "angular"实战应用看到这里,有的同学可能要问了,你既然说 联合类型 可以批量操作类型,那我想把某一组类型批量映射成另一种类型,该怎么操作呢?方法其实有很多,这里提供一种思路,抛砖引玉一下,别的方法就留给同学们自行研究吧。其实分析一下上面那个需求,不难看出,这个需求其实和数组的 map 方法有点相似那么如何实现一个操作 联合类型 的 map 函数呢?// 这里的 placeholder 可以键入任何你所希望映射成为的类型type UnionTypesMap = T extends any 'placeholder' : never;其实这里聪明的同学已经看出来,我们只是利用了 条件类型,使其的判断条件总是为 true,那么它就总是会返回左边的类型,我们就可以拿到 泛型操作符 的入参并自定义我们的操作。让我们趁热打铁,再举个具体的栗子:把 联合类型 的每一项映射成某个函数的 返回值

type UnionTypesMap2Func = T extends any=> T : never;type myUnionTypes = "vue" | "react" | "angular";type myUnionTypes2FuncResult = UnionTypesMap2FuncnTypes>;// => "vue") | => "react") | => "angular")相信有了上述内容的学习,我们已经对 联合类型 有了一个相对全面的了解,后续在此基础之上在作一些高级的拓展,也如砍瓜切菜一般简单了。其他数据类型当然除了数组,还存在其他的数据类型,例如可以用 typeinterface 模拟 Javascript 中的 字面量对象,其特征之一就是可以使用 myType['propKey'] 这样的方式取出子类型。这里抛砖引玉一下,有兴趣的同学可以自行研究。

Typescript 的类型是支持 "作用域" 的全局作用域就像常见的编程语言一样,在 Typescript 的类型系统中,也是支持 全局作用域 的。换句话说,你可以在没有 导入 的前提下,在 任意文件任意位置 直接获取到并且使用它。通常使用 declare 关键字来修饰,例如我们常见的 图片资源 的类型定义: declare module '*.png';declare module '*.svg';declare module '*.jpg';当然我们也可以在 全局作用域 内声明一个类型:declare type str = string;declare interface Foo { propA: string; propB: number;}需要注意的是,如何你的模块使用了 export 关键字导出了内容,上述的声明方式可能会失效,如果你依然想要将类型声明到全局,那么你就需要显式地声明到全局:declare global { const ModuleGlobalFoo: string;}模块作用域就像 nodejs 中的模块一样,每个文件都是一个模块,每个模块都是独立的模块作用域。这里模块作用域触发的条件之一就是使用 export 关键字导出内容。

每一个模块中定义的内容是无法直接在其他模块中直接获取到的,如果有需要的话,可以使用 import 关键字按需导入。泛型操作符作用域&函数作用域泛型操作符是存在作用域的,还记得这一章的第一节为了方便大家理解,我把泛型操作符类比为函数吗?既然可以类比为函数,那么函数所具备的性质,泛型操作符自然也可以具备,所以存在泛型操作符作用域自然也就很好理解了。这里定义的两个同名的 T 并不会相互影响:type TypeOperator = T;type TypeOperator2 = T;上述是关于泛型操作符作用域的描述,下面我们聊一聊真正的函数作用域:类型也可以支持闭包function Foo { return function {return param; }}const myFooStr = Foo;// const myFooStr: => string// 这里触发了闭包,类型依然可以被保留const myFoonum = Foo;// const myFooNum: => number// 这里触发了闭包,类型也会保持相互独立,互不干涉Typescript 的类型是支持 "递归" 的Typescript 中的类型也是可以支持递归的,递归相关的问题比较抽象,这里还是举例来讲解,同时为了方便大家的理解,我也会像第一节一样,把类型递归的逻辑用 Javascript 语法描述一遍。

首先来让我们举个栗子:假如现在需要把一个任意长度的元组类型中的子类型依次取出,并用 `&` 拼接并返回。这里解决的方法其实非常非常多,解决的思路也非常非常多,由于这一小节讲的是 递归,所以我们使用。

 
友情链接
鄂ICP备19019357号-22