写点什么

TypeScript 那些最佳实践

用户头像
思诚^_^
关注
发布于: 2 小时前


TypeScript 诞生已久,优缺点大家都知晓,它可以说是 JavaScript 静态类型校验和语法增强的利器,为了更好的代码可读性和可维护性,我们一个个老工程都坦然接受了用 TypeScript 重构的命运。然而在改造的过程中,逐步意识到 TypeScript 这门语言的艺术魅力


人狠话不多,下面我们先来聊一下 TypeScript 类型声明相关的技巧:

先了解 TypeScript 的类型系统

TypeScript 是 JavaScript 的超集,它提供了 JavaScript 的所有功能,并在这些功能的基础上附加一层:TypeScript 的类型系统



什么 TypeScript 的类型系统呢?举个简单的例子,JavaScript 提供了 String、Number、Boolean 等基本数据类型,但它不会检查变量是否正确地匹配了这些类型,这也是 JavaScript 弱类型校验语言的天生缺陷,此处可能会有人 DIS 弱类型语言的那些优点。但无可否认的是,很多大型项目里由于这种 弱类型的隐式转换 和 一些不严谨的判断条件 埋下了不胜枚举的 BUG,当然这不是我们今天要讨论的主题。


不同于 JavaScript,TypeScript 能实时检测我们书写代码里 变量的类型是否被正确匹配,有了这一机制我们能在书写代码的时候 就提前发现 代码中可能出现的意外行为,从而减少出错机会。 类型系统由以下几个模块组成:

推导类型

首先,TypeScript 可以根据 JavaScript 声明的变量 自动生成类型(此方式只能针对基本数据类型),比如:


const helloWorld = 'Hello World'  // 此时helloWorld的类型自动推导为string
复制代码

定义类型

再者,如果声明一些复杂的数据结构,自动推导类型的功能就显得不准确了,此时需要我们手动来定义 interface:


const helloWorld = { first: 'Hello', last: 'World' } // 此时helloWorld的类型自动推导为object,无法约束对象内部的数据类型
// 通过自定义类型来约束interface IHelloWorld { first: string last: string}const helloWorld: IHelloWorld = { first: 'Hello', last: 'World' }
复制代码

联合类型

可以通过组合简单类型来创建复杂类型。而使用联合类型,我们可以声明一个类型可以是许多类型之一的组合,比如:


type IWeather = 'sunny' | 'cloudy' | 'snowy'
复制代码

泛型

泛型是一个比较晦涩概念,但它非常重要,不同于联合类型,泛型的使用更加灵活,可以为类型提供变量。举个常见的例子:


type myArray = Array // 没有泛型约束的数组可以包含任何类型
// 通过泛型约束的数组只能包含指定的类型type StringArray = Array<string> // 字符串数组type NumberArray = Array<number> // 数字数组type ObjectWithNameArray = Array<{ name: string }> // 自定义对象的数组
复制代码


除了以上简单的使用,还可以通过声明变量来动态设置类型,比如:


interface Backpack<T> {  add: (obj: T) => void  get: () => T}declare const backpack: Backpack<string>console.log(backpack.get()) // 打印出 “string”
复制代码

结构类型系统

TypeScript 的核心原则之一是类型检查的重点在于值的结构,有时称为"duck typing" 或 "structured typing"。即如果两个对象具有相同的数据结构,则将它们视为相同的类型,比如:


interface Point {  x: number  y: number}
interface Rect { x: number y: number width: number height: number}
function logPoint(p: Point) { console.log(p)}const point: Point = { x: 1, y: 2 }const rect: Rect = { x:3, y: 3, width: 30, height: 50 }
logPoint(point) // 类型检查通过logPoint(rect) // 类型检查也通过,因为Rect具有Point相同的结构,从感官上说就是React继承了Point的结构
复制代码


此外,如果对象或类具有所有必需的属性,则 TypeScript 会认为它们成功匹配,而与实现细节无关

分清 type 和 interface 的区别

interface 和 type 都可以用来声明 TypeScript 的类型, 新手很容易搞错。我们先简单罗列一下两者的差异:



注意:由于 interface 支持同名类型自动合并,我们开发一些组件或工具库时,对于出入参的类型应该尽可能地使用 interface 声明,方便开发者在调用时做自定义扩展


从使用场景上说,type 的用途更加强大,不局限于表达 object/class/function ,还能声明基本类型别名、联合类型、元组等类型:


// 声明基本数据类型别名type NewString = string
// 声明联合类型interface Bird { fly(): void layEggs(): boolean}interface Fish { swim(): void layEggs(): boolean}type SmallPet = Bird | Fish
// 声明元组type SmallPetList = [Bird, Fish]
复制代码

3 个重要的原则

TypeScript 类型声明非常灵活,这也意味着一千个莎士比亚就能写出一千个哈姆雷特。在团队协作中,为了更好的可维护性, 我们应该尽可能地践行以下 3 条原则:

泛型优于联合类型

举个官方的示例代码做比较:


interface Bird {  fly(): void  layEggs(): boolean}interface Fish {  swim(): void  layEggs(): boolean}// 获得小宠物,这里认为不能够下蛋的宠物是小宠物。现实中的逻辑有点牵强,只是举个例子。function getSmallPet(...animals: Array<Fish | Bird>): Fish | Bird {  for (const animal of animals) {    if (!animal.layEggs())      return animal  }  return animals[0]}
let pet = getSmallPet()pet.layEggs() // okay 因为layEggs是Fish | Bird 共有的方法pet.swim() // errors 因为swim是Fish的方法,而这里可能不存在
复制代码


这种命名方式有 3 个问题:


  • 第一,类型定义使 getSmallPet变得局限。从代码逻辑看,它的作用是返回一个不下蛋的动物,返回的类型指向的是 Fish 或 Bird。但我如果只想在一群鸟中挑出一个不下蛋的鸟呢?通过调用这个方法,我只能得到一个 可能是 Fish、或者是 Bird 的神奇生物。

  • 第二,代码重复、难以扩展。比如,我想再增加一个乌龟,我必须找到所有类似 Fish | Bird 的地方,然后把它修改为 Fish | Bird | Turtle

  • 第三,类型签名无法提供逻辑相关性。我们再审视一下类型签名,完全无法看出这里为什么是 Fish | Bird 而不是其他动物,它们两个到底和逻辑有什么关系才能够被放在这里


介于以上问题,我们可以使用泛型重构一下上面的代码,来解决这些问题:


// 将共有的layEggs抽象到Eggable接口interface Eggable {  layEggs(): boolean}
interface Bird extends Eggable { fly(): void} interface Fish extends Eggable { swim(): void} function getSmallPet<T extends Eggable>(...animals: Array<T>): T { for (const animal of animals) { if (!animal.layEggs()) return animal } return animals[0]} let pet = getSmallPet<Fish>()pet.layEggs()pet.swim()
复制代码

巧用 typeof 推导优于自定义类型

这个技巧可以在没有副作用的代码中使用,最常见的是前端定义的常量数据结构。举个简单的 case,我们在使用 Redux 的时候,往往需要给 Redux 每个模块的 State 设置初始值。这个地方就可以用 typeof 推导出该模块的数据结构类型:


// 声明模块的初始stateconst userInitState = {  name: '',  workid: '',  avator: '',  department: '',}
// 根据初始state推导出当前模块的数据结构export type IUserStateMode = typeof userInitState // 导出的数据类型可以在其他地方使用
复制代码


这个技巧可以让我们非常坦然地 “偷懒”,同时也能减少一些 Redux 里的类型声明,比较实用

巧用内置工具函数优于重复声明

Typescript 提供的内置工具函数有如下几个:



上面几个工具函数尤其是 Partial、Pick、Exclude, Omit, Record 非常实用,平时在编写过程中可以做一些刻意练习

参考资料

用户头像

思诚^_^

关注

还未添加个人签名 2018.08.20 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
沙发
2 小时前
回复
没有更多了
TypeScript那些最佳实践