深刻 Typescript 类型系统

最近项目中准备推广接入 Typescript,抽空复习了一波相关的技术知识。说复习是由于以前看过了然而如今已经忘得一干二净了,除了不经常使用的缘由外,也是由于 Typescript 知识相对比较零散,学习时难成体系,因此趁这个机会整理整理,就当学习笔记吧。html

前言

Typescript (简称 TS)是 Javascript 的一个超集,它提供一套全面的对语法定义、使用的约束,来强化 JS 代码的可读性和可维护性,其中最重要的就是 TS 的类型系统。TS 类型系统提供了一套完备的声明和使用类型的方案,灵活但也复杂难记,致使 TS 的学习成本较高。前端

学习 TS 的类型系统,首先要记住很重要的一点:类型系统只在编译时起做用,最终必然不会出如今你的业务代码里。认识到这一点是很必要的,由于咱们学习 TS 很容易将它跟 JS 的语法代码放在一块来理解,其实这是不对的, TS 类型系统仅仅就是 TS 对 JS 的一种强化手段,学习 TS 类型系统,首先就是要忘记 JS,要把类型系统跟 JS 尽量划分开来,才能避免一些语法的理解干扰。数组

让类型更加灵活通用,是类型系统中一切语法特性的目标。独立地理解 TS 类型系统,也是基于这个目标由浅到深地了解。微信

从最简单的类型讲起

声明类型是使用类型的前提,TS 提供了许多用于声明类型的语法特性,其惟一目的就是不断完善和强化 TS 声明类型的能力,让类型声明更加地灵活、可复用。函数

咱们从最简单的类型开始来认识 TS,包括基础类型和高级类型。在 TS 中要声明一个类型,最简单的就是使用 type 关键字。工具

基础类型

基础类型对应了 JS 的多个基础的数据类型,主要包括:post

  • 原始类型:booleannumberstringvoidanynullundefined 以及 symbol
  • 引用类型:对象 Object 、函数 Function 以及数组 Array 等。

这是 TS 中最简单的数据类型了,咱们列举几个常常用到的:学习

// 声明的时候:
type _string = string;  // 字符串类型
type _number = number;  // 数字类型
type _boolean = boolean;  // 布尔值类型
type _any = any;  // 任意值
type _object = {  // 对象类型
  name: string;
  age: number
}
type _array = _object[];  // 数组类型
type _function = (user: _object) => number;  // 函数类型

// 使用的时候:
const str: _string = '111';
const num: _number = 3;
const show: _boolean = true;
let err: _any = -1;
err = 'something wrong';
const user: _object = {
  name: 'hankle',
  age: 23
}
const list: _array = [user];
const getAge: _function = (user) => {
  return user.age
}
复制代码

咱们声明了几个不一样的类型,并在使用的时候以 : [type] 的方式接入,进而对咱们的变量作了一层类型约束。从上边例子咱们也能够看出,type 能够给已有类型声明一个别名。ui

高级类型

高级类型,可让咱们更加灵活地重用已有的类型,是复用的一种有效手段。this

联合类型

联合类型表示取值能够为多种类型中的其中一种,使用 | 链接符,做用机制相似于 "或"。

type keyType = string | number;

const strKey: keyType = 'key';
const numKey: keyType = 1;
复制代码

键类型能够是 string,也能够是 number。

交叉类型

交叉类型表示变量应该具有多个类型的全部特性,使用 & 链接符,做用机制相似于 "并",多用于对象类型。在咱们须要对一些已定义好的 base 对象类型进行扩展时,可使用交叉类型来声明。

type User1 = {
  name: string;
  age: number
};
type User2 = {
  sex: number
}

const user: User1 & User2 = {
  name: 'hankle',
  age: 23,
  sex: 0
};
复制代码

_User1 & _User2 表示变量须要具有 _User1 和 _User2 中的全部属性。

字面量类型

type 声明的类型还能够是一个字面量,表示这个类型的取值只能是这个固定值,这就是字面量类型:

type coco = 'coco';
const str: coco = 'coco coco';
// 不能将类型“"coco coco"”分配给类型“"coco"”。
复制代码

有一种经常使用的字面量类型,叫「字符串字面量类型」,它结合联合类型进行使用,用来约束取值只能是某几个字符串中的一个:

type Key = 'name' | 'age';
type User = {
  name: string,
  age: number
}

const user: User = { name: 'hankle', age: 23 }
const getVal = (user: User, key: Key): any => user[key]

getVal(user, 'sex');
// 类型“"sex"”的参数不能赋给类型“Key”的参数。
复制代码

key 类型限制了对于 user 对象可取的属性键值,避免代码企图获取 user 中并不存在的属性。

接口:声明类型的另外一种方式

除了 type,在 TS 中还有另一种声明类型的方式,那就是接口(Interface)。使用 interface 关键字:

/** * 声明对象 * 等同于: * type User = { name: string; age: number } */
interface User {
  name: string;
  age: number
}

/** * 声明数组 * 等同于: * type List = User[] */
interface List {
  [index: number]: User
}

/** * 声明函数 * 等同于: * type findFunc = (list: List, name: string) => User */
interface findFunc {
  (list: List, name: string): User
}
复制代码

为何已经有了 type,还须要另外创造出「接口」来声明类型呢?事实上在不少 OOP 语言(好比 Java)中,接口是一个很重要的概念,用于对类(class)行为进行抽象。所以,TS 的接口除了描述对象类型、数组类型或函数类型的形状外,还能够描述类的形状。经过实现(implements)和继承(extends),接口定义的类型能够实现高度复用,这本就是 TS 类型系统的初衷。

在 TS 中,类(class)类型接口的声明和实现是这样的:

// 定义了一个报警类接口,要求实现该接口的类都必须拥有 alert 方法
interface Alarm {
    alert();
}
// SecurityDoor 类实现了该接口,并实现了 alert 方法
class SecurityDoor implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}
// Car 类没有实现 alert 方法,报错了
class Car implements Alarm {
}
// 类“Car”错误实现接口“Alarm”。
// Property 'alert' is missing in type 'Car' but required in type 'Alarm'.
复制代码

type 关键字声明的类型能够经过联合类型对类型进行扩展,而接口则是利用继承(extends)来实现的:

interface User1 {
  name: string;
  age: number
};
// 声明了接口 User2,继承自接口 User1,它须要包含 User1 的形状
interface User2 extends User1 {
  sex: number
}

const userq: User2 = {
  name: 'hankle',
  sex: 0
};
// Property 'age' is missing in type '{ name: string; sex: number; }' but required in type 'User2'.
复制代码

至于说何时使用 type,何时使用 interface ,网上却是有很多关于这个问题的解读。其实这两个能作的事情都大同小异,至于选用哪一个,我认为在平时业务场景开发中,若是可能是对一些函数结构、象结构、类结构做类型约束的话,使用 interface 会更加地语义化,建议多用 interface 来声明类型。

泛型:让你的类型更通用

若是现有一个 find 函数,做用等同于 Array.find,须要咱们本身声明它的函数类型,那到目前来讲,咱们可能只能利用函数的声明重载来这样写:

// 声明重载
function find(list: string[], func: (item: string) => boolean): string | null;
function find(list: number[], func: (item: number) => boolean): number | null;
function find(list: object[], func: (item: object) => boolean): object | null;
// 实现
function find(list, func) {
  return list.find(func)
}
复制代码

函数重载容许经过屡次声明函数的不一样形状来实现函数的联合类型。find 方法容许传入多种类型的数组,其返回值也将由传入数组所包含的元素类型决定。从上边能够看出,咱们重复声明的只是不一样的元素类型,函数的参结构是没有变化的,因此出现了不少重复代码。

函数的泛型

泛型(Generics)正是为了解决这个问题。顾名思义,泛型表明不肯定的类型,它容许复杂类型在声明的时候内部保留不肯定的类型,等到使用的时候再具体指定。泛型至关于一个函数,具体说是类型的工厂函数,既然是函数那就是要“传参”的,泛型传参使用<>实现。上边例子用泛型实现是这样的:

// find 的类型是一个泛型
function find<T>(list: T[], func: (item: T) => boolean): T | null {
  return list.find(func)
}

// 使用时再指定具体类型
find<number>([1,2,3], item => item > 2)
复制代码

固然,你也能够用接口来声明函数类型:

// find 的类型 FindType 是一个泛型
interface FindType {
  <T>(list: T[], func: (item: T) => boolean): T | null } const find: FindType = function (list, func) { return list.find(func) } // 使用时指定具体类型 find<number>([1,2,3], item => item > 2) 复制代码

泛型还能够容许指定多个类型参数:

// tuple元素互换
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap<number, string>([7, 'seven']); // ['seven', 7]
复制代码

类的泛型

泛型不只能够做用在函数类型上,还广泛地应用在类的类型定义中。和函数调用时再具体指定类型类似,类的泛型是在实例化对象时才具体指定的:

// 声明了一个泛型类
class EleList<T> {
  list: T[]
  add: (item: T) => number
}

// 实例化时,指定为字符串类型
const users = new EleList<string>();
users.list = ['hankle', 'nancy'];
users.add = function (item) {
  this.list.push(item);
  return this.list.length;
}
复制代码

泛型约束

泛型容许咱们预设和使用一个不肯定的类型,但有时候“不肯定”也对咱们的使用形成了影响,好比咱们有这样一个用于获取长度的工具方法,入参能够是一个数组,也能够是一个类数组对象:

function get<T>(arg: T): number {
  return arg.length; // 类型“T”上不存在属性“length”。
}
复制代码

然而咱们发现它报错了。类型 T 在这里是任意类型,咱们并不能保证使用时指定的类型都存在属性 length。咱们但愿T是有限制的,它只能是数组或类数组对象,或者更具体地说,它必须包含 length 属性。因此这时候,泛型约束派上用场了:

// 声明 LengthType 类型,必须包含 length 属性
interface LengthType {
  length: number;
}

// T 继承自 LengthType 类型,也就代表它必须具备 length 属性
function get<T extends LengthType>(arg: T): number {
  return arg.length;
}
复制代码

这里咱们声明了一个具备 length 属性的类型,而后让类型 T 继承该类型,从而对泛型的结构进行约束。咱们说过接口继承也使用了 extends 关键字,它的深层含义就是继承者必须包含被继承者的全部特性,因此泛型约束一样也是借助 extends 的这个做用来限制泛型的结构形状。泛型约束能够帮助咱们减小许多无心义的判断逻辑,在实际开发过程当中,咱们的泛型每每都不是彻底的任意类型,所以应当善于使用泛型约束。

高阶类型:泛型还能这么玩

泛型能够是 TS 类型系统最值得利用的特性了。前边说过,泛型是一个类型的工厂函数,换句话说,利用泛型能够进一步构建出各式各样的类型。TS 官方把这类类型一样称为高级类型,而我认为把它们叫作高阶类型,更为合适。

映射类型

映射类型就是一种经常使用的泛型结构。映射类型能对一个旧类型的各项属性进行映射,进而生成一个新类型。TS 标准库已经内置了多个十分实用的映射类型,包括 PartialRequiredReadonlyPickRecord等,咱们挑选几个解释一下。

Partial<T>

type Partial<T> = {
    [P in keyof T]?: T[P];
};
复制代码

keyof 返回的是 T 中全部属性键名组成的字符串字面量类型,因此 Partial 用于把传入类型 T 中的各项属性转化为可选属性。

interface User {
  name: string;
  age: number;
  sex: number;
};

// User 类型的属性是必选的,PartialUser 类型的属性是可选的
type PartialUser = Partial<User>
/** * type PartialUser = { * name?: string; * age?: number; * sex?: number; * } */
复制代码

Required<T>

type Required<T> = {
    [P in keyof T]-?: T[P];
};
复制代码

Partial 相反,Required 用于把传入类型 T 中的各项属性转化为必选属性。

interface User {
  name?: string;
  age?: number;
  sex?: number;
};

// User 类型的属性是可选的,RequiredUser 类型的属性是必选的
type RequiredUser = Required<User>
/** * type RequiredUser = { * name: string; * age: number; * sex: number; * } */
复制代码

Readonly<T>

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
复制代码

Readonly 用于将传入类型 T 中的全部属性转化为只读属性。

interface User {
  name: string;
  age: number;
  sex: number;
};

// User 类型的属性是可写的,ReadonlyUser 类型的属性是只读的
type ReadonlyUser = Readonly<User>
/** * type ReadonlyUser = { * readonly name: string; * readonly age: number; * readonly sex: number; * } */
复制代码

Pick<T, K>

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
复制代码

Pick 用于根据传入类型生成一个包含指定属性的新类型,也就是说,咱们能够从旧类型中挑出部分属性来构成一个新的类型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// PickUser 类型没有 sex 属性
type PickUser = Pick<User, 'name' | 'age'>
/** * type PickUser = { * name: string; * age: number; * } */
复制代码

Record<T, K>

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
复制代码

Record 利用一个字符串字面量类型生成一个对象类型,字符串字面量类型的各项将做为对象类型的属性键名。不一样于以上几个类型,Record 不是同态的,即它不须要输入类型来拷贝属性,而是直接建立新的属性。

type RecordUser = Record<'name' | 'telephone' | 'description', string>
/** * type RecordUser = { * name: string; * telephone: string; * description: string; * } */
复制代码

条件类型

另外一种经常使用的泛型结构是条件类型。条件类型能在生成一个新类型时对类型的组合转换提供条件限制。一样的,TS 标准库也内置了一些经常使用的条件类型,包括 ExcludeExtractReturnTypeParameters等。

Extract<T, U>

type Extract<T, U> = T extends U ? T : never
复制代码

Extract<T, U> 用于从联合类型 T 中排除联合类型 U 中不存在的成员,生成一个新的联合类型,也就是说生成的联合类型只包含那些 T 有且 U 也有的成员,做用相似于求交集。

type UserKey = Extract<'name' | 'age' | 'sex', 'name' | 'sex' | 'descrption'>
// type UserKey = "name" | "sex"
复制代码

Exclude<T, U>

type Exclude<T, U> = T extends U ? never : T
复制代码

Exclude<T, U> 用于从联合类型 T 中排除联合类型 U 中存在的成员,生成一个新的联合类型,也就是说生成的联合类型只包含那些 T 有而 U 没有的成员,做用相似于求非集。

type UserKey = Exclude<'name' | 'age' | 'sex', 'sex'>
// type UserKey = "name" | "age"
复制代码

利用 Exclude,咱们还能够实现一个和 Pick 做用相反的 NeverPick

type NeverPick<T, U> = {
  [P in Exclude<keyof T, U>]: T[P];
};

interface User {
  name: string;
  age: number;
  sex: number;
};

type NeverPickUser = NeverPick<User, 'sex'>
/** * type NeverPickUser = { * name: string; * age: number; * } */
复制代码

Parameters<T>

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
复制代码

Parameters 当传入的指定类型为函数类型时,返回函数入参的类型。这里使用了 infer 操做符来表示待推断类型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// 定义一个函数类型 GetUserFunc,入参 name 的类型是 string
interface GetUserFunc {
  (name: string): User 
}

// 提取到了 GetUserFunc 的参数类型
type funcParamsTypes = Parameters<GetUserFunc>
// type funcParamsTypes = [string]
复制代码

ReturnType<T>

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
复制代码

ReturnType 当传入的指定类型为函数类型时,返回函数返回值的类型。

interface User {
  name: string;
  age: number;
  sex: number;
};

// 定义一个函数类型 GetUserFunc,返回值的类型是 User
interface GetUserFunc {
  (name: string): User 
}

// 提取到了 GetUserFunc 的参数类型
type funcReturnTypes = ReturnType<GetUserFunc>
// type funcReturnTypes = User
复制代码

参考文章

Typescript官方文档-高级类型
解读TypeScript中的泛型以及条件类型中的推断


若是你以为这篇内容对你有价值,欢迎点赞并关注咱们前端团队的 官网 和咱们的微信公众号 WecTeam,每周都有优质文章推送~

相关文章
相关标签/搜索