对于有 JavaScript 基础的同窗来讲,入门 TypeScript 其实很容易,只须要简单掌握其基础的类型系统就能够逐步将 JS 应用过渡到 TS 应用。react
// js const double = (num) => 2 * num // ts const double = (num: number): number => 2 * num
然而,当应用愈来愈复杂,咱们很容易把一些变量设置为 any 类型,TypeScript 写着写着也就成了 AnyScript。为了让你们能更加深刻的了解 TypeScript 的类型系统,本文将重点介绍其高级类型,帮助你们摆脱 AnyScript。程序员
在讲解高级类型以前,咱们须要先简单理解泛型是什么。typescript
泛型是强类型语言中比较重要的一个概念,合理的使用泛型能够提高代码的可复用性,让系统更加灵活。下面是维基百科对泛型的描述:数组
泛型容许程序员在强类型程序设计语言中编写代码时使用一些之后才指定的类型,在实例化时做为参数指明这些类型。
泛型经过一对尖括号来表示(<>
),尖括号内的字符被称为类型变量,这个变量用来表示类型。函数
function copy<T>(arg: T): T { if (typeof arg === 'object') { return JSON.parse( JSON.stringify(arg) ) } else { return arg } }
这个类型 T,在没有调用 copy 函数的时候并不肯定,只有调用 copy 的时候,咱们才知道 T 具体表明什么类型。工具
const str = copy<string>('my name is typescript')
咱们在 VS Code 中能够看到 copy 函数的参数以及返回值已经有了类型,也就是说咱们调用 copy 函数的时候,给类型变量 T 赋值了 string。其实,咱们在调用 copy 的时候能够省略尖括号,经过 TS 的类型推导是能够肯定 T 为 string 的。学习
除了 string、number、boolean 这种基础类型外,咱们还应该了解一些类型声明中的一些高级用法。fetch
交叉类型说简单点就是将多个类型合并成一个类型,我的感受叫作「合并类型」更合理一点,其语法规则和逻辑 “与” 的符号一致。ui
T & U
假如,我如今有两个类,一个按钮,一个超连接,如今我须要一个带有超连接的按钮,就可使用交叉类型来实现。es5
interface Button { type: string text: string } interface Link { alt: string href: string } const linkBtn: Button & Link = { type: 'danger', text: '跳转到百度', alt: '跳转到百度', href: 'http://www.baidu.com' }
联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为链接的多个类型中的任意一个。
T | U
例如,以前的 Button 组件,咱们的 type 属性只能指定固定的几种字符串。
interface Button { type: 'default' | 'primary' | 'danger' text: string } const btn: Button = { type: 'primary', text: '按钮' }
前面提到的交叉类型与联合类型若是有多个地方须要使用,就须要经过类型别名的方式,给这两种类型声明一个别名。类型别名与声明变量的语法相似,只须要把 const
、let
换成 type
关键字便可。
type Alias = T | U
type InnerType = 'default' | 'primary' | 'danger' interface Button { type: InnerType text: string } interface Alert { type: ButtonType text: string }
keyof
相似于 Object.keys
,用于获取一个接口中 Key 的联合类型。
interface Button { type: string text: string } type ButtonKeys = keyof Button // 等效于 type ButtonKeys = "type" | "text"
仍是拿以前的 Button 类来举例,Button 的 type 类型来自于另外一个类 ButtonTypes,按照以前的写法,每次 ButtonTypes 更新都须要修改 Button 类,若是咱们使用 keyof
就不会有这个烦恼。
interface ButtonStyle { color: string background: string } interface ButtonTypes { default: ButtonStyle primary: ButtonStyle danger: ButtonStyle } interface Button { type: 'default' | 'primary' | 'danger' text: string } // 使用 keyof 后,ButtonTypes修改后,type 类型会自动修改 interface Button { type: keyof ButtonTypes text: string }
这里的 extends
关键词不一样于在 class 后使用 extends
的继承做用,泛型内使用的主要做用是对泛型加以约束。咱们用咱们前面写过的 copy 方法再举个例子:
type BaseType = string | number | boolean // 这里表示 copy 的参数 // 只能是字符串、数字、布尔这几种基础类型 function copy<T extends BaseType>(arg: T): T { return arg }
若是咱们传入一个对象就会有问题。
extends
常常与 keyof
一块儿使用,例如咱们有一个方法专门用来获取对象的值,可是这个对象并不肯定,咱们就可使用 extends
和 keyof
进行约束。
function getValue<T, K extends keyof T>(obj: T, key: K) { return obj[key] } const obj = { a: 1 } const a = getValue(obj, 'a')
这里的 getValue 方法就能根据传入的参数 obj 来约束 key 的值。
in
关键词的做用主要是作类型的映射,遍历已有接口的 key 或者是遍历联合类型。下面使用内置的泛型接口 Readonly
来举例。
type Readonly<T> = { readonly [P in keyof T]: T[P]; }; interface Obj { a: string b: string } type ReadOnlyObj = Readonly<Obj>
咱们能够结构下这个逻辑,首先 keyof Obj
获得一个联合类型 'a' | 'b'
。
interface Obj { a: string b: string } type ObjKeys = 'a' | 'b' type ReadOnlyObj = { readonly [P in ObjKeys]: Obj[P]; }
而后 P in ObjKeys
至关于执行了一次 forEach 的逻辑,遍历 'a' | 'b'
type ReadOnlyObj = { readonly a: Obj['a']; readonly b: Obj['b']; }
最后就能够获得一个新的接口。
interface ReadOnlyObj { readonly a: string; readonly b: string; }
条件类型的语法规则和三元表达式一致,常常用于一些类型不肯定的状况。
T extends U ? X : Y
上面的意思就是,若是 T 是 U 的子集,就是类型 X,不然为类型 Y。下面使用内置的泛型接口 Extract
来举例。
type Extract<T, U> = T extends U ? T : never;
若是 T 中的类型在 U 存在,则返回,不然抛弃。假设咱们两个类,有三个公共的属性,能够经过 Extract 提取这三个公共属性。
interface Worker { name: string age: number email: string salary: number } interface Student { name: string age: number email: string grade: number } type CommonKeys = Extract<keyof Worker, keyof Student> // 'name' | 'age' | 'email'
TypesScript 中内置了不少工具泛型,前面介绍过 Readonly
、Extract
这两种,内置的泛型在 TypeScript 内置的 lib.es5.d.ts
中都有定义,因此不须要任何依赖都是能够直接使用的。下面看看一些常用的工具泛型吧。
type Partial<T> = { [P in keyof T]?: T[P] }
Partial
用于将一个接口的全部属性设置为可选状态,首先经过 keyof T
,取出类型变量 T
的全部属性,而后经过 in
进行遍历,最后在属性后加上一个 ?
。
咱们经过 TypeScript 写 React 的组件的时候,若是组件的属性都有默认值的存在,咱们就能够经过 Partial
将属性值都变成可选值。
import React from 'react' interface ButtonProps { type: 'button' | 'submit' | 'reset' text: string disabled: boolean onClick: () => void } // 将按钮组件的 props 的属性都改成可选 const render = (props: Partial<ButtonProps> = {}) => { const baseProps = { disabled: false, type: 'button', text: 'Hello World', onClick: () => {}, } const options = { ...baseProps, ...props } return ( <button type={options.type} disabled={options.disabled} onClick={options.onClick}> {options.text} </button> ) }
type Required<T> = { [P in keyof T]-?: T[P] }
Required
的做用恰好与 Partial
相反,就是将接口中全部可选的属性改成必须的,区别就是把 Partial
里面的 ?
替换成了 -?
。
type Record<K extends keyof any, T> = { [P in K]: T }
Record
接受两个类型变量,Record
生成的类型具备类型 K 中存在的属性,值为类型 T。这里有一个比较疑惑的点就是给类型 K 加一个类型约束,extends keyof any
,咱们能够先看看 keyof any
是个什么东西。
大体一直就是类型 K 被约束在 string | number | symbol
中,恰好就是对象的索引的类型,也就是类型 K 只能指定为这几种类型。
咱们在业务代码中常常会构造某个对象的数组,可是数组不方便索引,因此咱们有时候会把对象的某个字段拿出来做为索引,而后构造一个新的对象。假设有个商品列表的数组,要在商品列表中找到商品名为 「每日坚果」的商品,咱们通常经过遍历数组的方式来查找,比较繁琐,为了方便,咱们就会把这个数组改写成对象。
interface Goods { id: string name: string price: string image: string } const goodsMap: Record<string, Goods> = {} const goodsList: Goods[] = await fetch('server.com/goods/list') goodsList.forEach(goods => { goodsMap[goods.name] = goods })
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
Pick
主要用于提取接口的某几个属性。作过 Todo 工具的同窗都知道,Todo工具只有编辑的时候才会填写描述信息,预览的时候只有标题和完成状态,因此咱们能够经过 Pick
工具,提取 Todo 接口的两个属性,生成一个新的类型 TodoPreview。
interface Todo { title: string completed: boolean description: string } type TodoPreview = Pick<Todo, "title" | "completed"> const todo: TodoPreview = { title: 'Clean room', completed: false }
type Exclude<T, U> = T extends U ? never : T
Exclude
的做用与以前介绍过的 Extract
恰好相反,若是 T 中的类型在 U 不存在,则返回,不然抛弃。如今咱们拿以前的两个类举例,看看 Exclude
的返回结果。
interface Worker { name: string age: number email: string salary: number } interface Student { name: string age: number email: string grade: number } type ExcludeKeys = Exclude<keyof Worker, keyof Student> // 'salary'
取出的是 Worker 在 Student 中不存在的 salary
。
type Omit<T, K extends keyof any> = Pick< T, Exclude<keyof T, K> >
Omit
的做用恰好和 Pick 相反,先经过 Exclude<keyof T, K>
先取出类型 T 中存在,可是 K 不存在的属性,而后再由这些属性构造一个新的类型。仍是经过前面的 Todo 案例来讲,TodoPreview 类型只须要排除接口的 description 属性便可,写法上比以前 Pick 精简了一些。
interface Todo { title: string completed: boolean description: string } type TodoPreview = Omit<Todo, "description"> const todo: TodoPreview = { title: 'Clean room', completed: false }
若是只是掌握了 TypeScript 的一些基础类型,可能很难游刃有余的去使用 TypeScript,并且最近 TypeScript 发布了 4.0 的版本新增了更多功能,想要用好它只能不断的学习和掌握它。但愿阅读本文的朋友都能有所收获,摆脱 AnyScript。