4

JafeneyLou的个人空间

 2 years ago
source link: https://my.oschina.net/u/3842749/blog/5168719
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

头图

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 的类型, 新手很容易搞错。我们先简单罗列一下两者的差异:

对比项 type interface 类型合并方式 只能通过&进行合并 同名自动合并,通过extends扩展 支持的数据结构 所有类型 只能表达 object/class/function 类型

注意:由于 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推导出该模块的数据结构类型:

// 声明模块的初始state
const userInitState = {
  name: '',
  workid: '',
  avator: '',
  department: '',
}

// 根据初始state推导出当前模块的数据结构
export type IUserStateMode = typeof userInitState // 导出的数据类型可以在其他地方使用

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

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

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

内置函数 用途 例子 Partial<T> 类型T的所有子集(每个属性都可选) Partial<IUserStateMode> Readony<T> 返回和T一样的类型,但所有属性都是只读 Readony<IUserStateMode> Required<T> 返回和T一样的类型,每个属性都是必须的 Required<IUserStateMode> Pick<T, K extends keyof T> 从类型T中挑选的部分属性K `Pick<IUserStateMode, 'name' Exclude<T, U extends keyof T> 从类型T中移除部分属性U `Exclude<IUserStateMode, 'name' NonNullable<T> 从属性T中移除null和undefined NonNullable<IUserStateMode> ReturnType<T> 返回函数类型T的返回值类型 ReturnType<IUserStateMode> Record<K, T> 生产一个属性为K,类型为T的类型集合 Record<keyof IUserStateMode, string> Omit<T, K> 忽略T中的K属性 Omit<IUserStateMode, 'name'>

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

本文由博客一文多发平台 OpenWrite 发布!如果这篇文章帮助到您,记得帮忙点个赞哦~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK