共计 4503 个字符,预计需要花费 12 分钟才能阅读完成。
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’ | ‘workid’ | ‘avator’>` |
Exclude<T, U extends keyof T> |
从类型 T 中移除局部属性 U | `Exclude<IUserStateMode, ‘name’ | ‘department’>` | |
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 十分实用,平时在编写过程中能够做一些刻意练习
参考资料
- 「TypeScript 中高级利用与最佳实际」