likes
comments
collection
share

1.4w字总结带你重学TypeScriptTypeScript是JavaScript类型的超集,它可以编译成纯JavaS

作者站长头像
站长
· 阅读数 18

学习环境搭建。

1,创建一个文件夹,创建文件 src/index.ts, 内部执行 npm init -y 随后会生成 package.json

2,安装相关包 npm install typescript ts-node nodemon

3,执行 tsc --init

4,package.json 中新增一条命令如下

"scripts": {
  "dev": "nodemon --watch ./src -e ts --exec ts-node ./src/index.ts"
}

大致为 nodemon 侦听 src 下的所有 ts 文件并且执行这个 index.ts 文件。index.ts 写入的代码在执行脚本后都会被执行。

或者可以使用在线的方式学习。在线学习

TS 基础类型

TS 在书写基本类型时通常为 变量: 类型 这种方式也被成为类型注解。

字符串 (string)

const str: string = 'hello';

数字 (number)

const num: number = 123;

布尔 (boolean)

const isDone: boolean = true;

数组 (Array)

TS 中有两种定义数组的方式。

// 第一种 在类型后面跟 []
const arr1: number[] = [1, 2, 3, 4, 5];
// 第二种 数组泛型 Array<string>
const arr2: Array<string> = ['1', '2', '3', '4'];

大整数 (BigInt)

注意: 如果要使用此种数据类型需要将 tsconfig.json 中的 lib 选项解注释并加入以下内容 lib: ["ESNext"] 或者 ES2020

const big: BigInt = BigInt(Number.MAX_SAFE_INTEGER);

Symbol

注意: symbol 是 ES6 才出现的类型,如果要使用此类语法仍然需要调整 lib 选项。

const symbolId: symbol = Symbol();

对象类型 (object)

object 个人理解只要是 typeof 返回为对象的类型都可以使用除了 null。但是他也会有一些缺点。

const obj: object = {};
const obj1: object = [];
const obj2: object = null; // 不能将类型“null”分配给类型“object”

// 此种情况下访问 user 属性时,会丢失提示功能。对于有值的对象一般不会这样使用。
const user: object = { name: 'zhangsan', age: 24 };

与此对应的还有一个 Object 也可以当做类型。只要拥有 Object 构造函数原型方法的类型都可以使用该类型。

let str: Object = '';
let num: Object = 1;
let isDone: Object = true;

null undefined

在 TS 中 nullundefined 即是值也是一种类型。

const u: undefined = undefined;
const n: null = null;

元祖 (tuple)

和数组类似,但是其中的类型和顺序以及数量都需要被提前定义。

let tuple: [string, number] = ['', 1];
ctuple = ['', 1, 1]; // 不能将类型“[string, number, number]”分配给类型“[string, number]”。
tuple[3]; // 长度为 "2" 的元组类型 "[string, number]" 在索引 "3" 处没有元素

// 可变元祖 确定开头的类型,后续类型放宽
let tuple: [string, number, ...any] = ['1', 2, 3, 4, 5];
// 元祖解构
let { name, age, ...rest } = tuple; // 虽然取到了值,但是可读性并不强。
console.log(rest); // 3, 4, 5
// 元祖标签解决结构的可读性问题
let tuple: [name: string, age: number, ...rest: any] = ['1', 2, 3, 4, 5];
let { name, age, rest } = tuple;

任意类型 (any)

如果说上述类型都是 ES6 中自有的,只是写法上些许不同。那么从本类型开始,都是 TS 的类型了。

any 代表任意的一种类型。使用此类型之后同样会失去代码提示。当然强大如 TS 在编写代码的时候也不可能明确每个值的类型。虽然失去的提示功能,同时也意味着我们可以随意的调用任何方法等等。。。

同时 any 可以赋值给任意值,也可以接受任意类型的值。

let num: any = '';
num.getFirstStr(); // 只有在执行阶段才会出错。

(unknown)

说起 any 自然离不开 unknown TS 中的顶级类型。于 any 的不同点在于 unknown 只能接受 unknownany

// 对于赋值时 any 特性一致
let value: unknown;
value = 1;
value = '1';
value = true;
value = [];
value = {};
value = Symbol();
value = undefined;
value = null;
value = new TypeError();

// 反过来 unknown 就只能接受 unknown 和 any 类型
let value1: string = value; // 不能将类型“unknown”分配给类型“string”。
let value2: unknown = value;
let value3: any = value;

// 类型收窄
let u: unknown;
let a: any;
u.toFixed(); // Error 类型“unknown”上不存在属性“toFixed”。
a.toFixed();

if (typeof u === 'object') {
  u.toFixed();
}

枚举 (enmu)

enmu 类型是对 JavaScript 标准数据类型的一个补充。

假如我们现在有个需求,一个值有 3 个状态,根据不同的状态执行不同的分支代码。没有 enmu 时我们可能会写出以下代码。

const STATE = {
  SUCCESS: 'SUCCESS',
  FIAL: 'FIAL',
  RUNNING: 'RUNNING'
};
// 使用 enmu 写法。当然访问的时候和上面一样,State.xxx
enum State {
  SUCCESS = 'SUCCESS',
  FIAL = 'FIAL',
  RUNNING = 'RUNNING'
}
// 除了以上字面量的方式,也可以不定义任何值。
enum Index {
  Zero,
  One,
  Two
} // 如果没有赋值,此时默认值从 0 开始依次递增。
enum resCode {
  Success = 200,
  Redirect
} // 如果初始值赋值为数字,第二个默认值为初始值的递增, 如果初始值为字符串则会报错:枚举成员必须具有初始化表达式。

没有任何类型 (void)

一般用于返回值类型。也可以声明一个 void 的类型的变量但是他只能赋值给 undefinednull。赋值仅限于 tsconfig.json 中的 strict 选项关闭时的情况。如果没有关闭 void 只能赋值给 undefined

function Test(): void {}
let v: void = null;
v = undefined;

不存在的类型 (never)

抛出异常,或者永远不会有返回值的函数以及永远不会为真的类型返回 never

// 抛出异常
function error(): never {
  throw new Error('');
}
// 永远不会有返回值
function infiniteLoop(): never {
  while (true) {}
}
// & 交叉类型类似于 && 运算符。 此时 n 被推导成 never 类型
type n = string & number;

函数

在 JS 中单独书写函数时,常用的有 函数表达式,箭头函数,命名函数等等...

// 函数表达式
let func = function (name: string, age: number): void {
  console.log(name, age);
};
// 箭头函数
func = (name: string, age: number): void => {
  console.log(name, age);
};
// 命名函数
function func(name: string, age: number): void {
  console.log(name, age);
}

函数类型

let func: (name: string, age: number) => void = function (name: string, age: number) {
  console.log(name, age);
};
// 拆解
// 1, let func: (name: string, age: number) => void 冒号后面为类型
// 2, func = function (name: string, age: number) {} 函数体

可选参数、默认参数

// 可选参数类型
let func: (name: string, age?: number) => void;

// 可选参数一定要在必选参数的后面。
func = function (name: string, age?: number): void {
  console.log(name, age);
};
func('zhangsan'); // zhangsan undefined
// 默认参数一定要在必选参数的后面。
func = function (name: string, age: number = 24): void {
  console.log(name, age);
};
func('zhangsan'); // zhangsan 24

剩余参数

let func: (name: string, age: number, ...rest: string[]) => void;
func = function (name: string, age: number, ...rest: string[]) {
  console.log(rest);
};
func('zhangsan', 24, 'boy', 'unknown'); // [ 'boy', 'unknown' ]

函数重载

const user = [{ userId: 1, username: 'zhangsan' }];

// 需要注意的是只有最后一个才是真正的函数,其他的地方只能是函数类型的定义。
function getUser(userId: number): void;
function getUser(username: string): void;
function getUser(value: any) {
  let userInfo;
  if (typeof value === 'string') {
    userInfo = user.find(item => item.username === value);
  }
  if (typeof value === 'number') {
    userInfo = user.find(item => item.userId === value);
  }
}

这里主要总结 ES6 中没有的新特性。在总结之前带着 TS 特有的类型简单复习下如何书写一个类。

// 声明一个类
class Person {
  // 实例属性 public 默认带着 可写可不写
  public name: string;
  public age: number;
  // 只读属性
  readonly sex: string = '男';
  // 静态属性
  static money: number = 100;
  // 构造函数
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  // 静态方法
  static howMoney(): void {
    console.log('开始数钱。。。');
  }
  // 实例方法
  public getUser(): void {
    console.log('获取用户。。。');
  }
}

// 继承类
class User extends Person {
  constructor(name: string, age: number) {
    // 继承类中必须调用 super
    super(name, age);
  }
  static howMoney(): void {
    // 子类中调用父类方法必须用super
    super.howMoney();
    console.log('数钱结束。。。');
  }
  public getUser(): void {
    super.getUser();
    console.log('hello', this.name);
  }
}
const user = new User('zhangsan', 24);
user.getUser();
User.howMoney();

更简单的书写一个类

// 1, 当我们没有书写构造函数时 就会出现下面的错误解决方案如下
class Person {
  name: string; // Error 属性“name”没有初始化表达式,且未在构造函数中明确赋值
  _name!: string; // OK ! 表示这个值一定不是空的
  name_?: string; // OK ? 表示这个值可能是空的
}

// 2, 每次写一个类都是先声明属性,然后在 constructor 中 this.xx = xx; 太繁琐了?
class User {
  // 这样一行相当于原始写法先声明后赋值。不过可读性不太好。
  constructor(public name: string, age: number) {}
}

类的修饰符

像我们开头写的 publicstaticreadonly 都被称为修饰符。在 TS 中除了这三种修饰符又新增了两个。分别是 protectedprivate

如果要给它们分类的话应该是。

  • public : 公共属性、方法,可以在实例以及子类中访问。

  • protected : 只能在子类中访问该属性和方法。

  • private : 只能在类中访问被修饰符修饰的方法和属性。

class Person {
  protected age!: number;
  private name!: string;
  public sex!: string;

  private getName() {}
  protected getAge() {}
  public getSex() {}

  // name 和 getName() 只能在 Person 中用 this 访问
}
class User extends Person {
  constructor() {
    super();
    // User中可以访问 getAge() 和 age 以及被 public 修饰的属性以及方法
  }
}
const person = new Person(); // person 只能访问到 getSex() 和 sex
  • static : 定义类中的静态属性以及方法。

  • readonly : 定义类型为只读属性。注意:只能定义属性。

// 可以组合使用,类似于以下方式
class User {
  public static name!: string;
  public readonly age!: number;
}

存取器

TS 支持通过 getter/setter 来截取对象成员的访问。

假设我们现在有个类,其中 name 是私有属性,但是我们又想要更改以及访问这个属性,此时就可以使用存取器。

class User {
  private _name: string;
  constructor(_name: string) {
    this._name = _name;
  }
  get name(): string {
    return this._name;
  }
  set name(val: string) {
    this._name = val;
  }
}
const user = new User('zhangsan');
console.log(user.name); // zhangsan
user.name = 'lisi';
console.log(user.name); // lisi

抽象类 (abstract)

abstract class Person {
  abstract name: string;
  // 第一种声明抽象方法的方式 推荐
  abstract getName(): void;
  // 第二种声明抽象方法的方式
  abstract getAge: () => void;
}
// 实现抽象类可以使用 implements 关键字
class User implements Person {
  name!: string;
  getName(): void {
    throw new Error('Method not implemented.');
  }
}

注意:

1, 抽象类不能用于声明构造函数 constructor

2, 抽象类中可以有非抽象方法和非抽象属性。

3, 抽象中的属性不可以使用 ! 修饰符。

abstract class Person {
  sex!: string; // 2, OK
  abstract age!: string; // 3, 此上下文中不允许明确的赋值断言 "!"
  abstract constructor(public name: string) {} // 1, "abstract" 修饰符仅可出现在类、方法或属性声明中。
  // 2, OK
  getName(): void {
    console.log('zhangsan');
  }
}

除了通过 implements 去实现一个抽象类,我们还可以通过 extends 去继承一个实现类。那他们有什么区别呢?

abstract class Person {
  age!: number;
  abstract name?: string;
  abstract getName(): void;
  getAge() {}
}
// 1, 通过 extends 继承没有被 abstract 描述的方法以及属性可以不被实现也不会触发类型检查错误
class User extends Person {
  name?: string | undefined;
  getName(): void {
    throw new Error('Method not implemented.');
  }
  // 2, 如果被继承可以使用 super 调用该方法
  getAge(): void {
    super.getAge();
  }
}
// 3, 通过 implements 实现抽象类时,必须实现里面所有的方法以及属性。
class User1 implements Person {
  age!: number;
  name?: string | undefined;
  getName(): void {
    throw new Error('Method not implemented.');
  }
  getAge(): void {
    throw new Error('Method not implemented.');
  }
}

类类型

之前我们每次都是 const user = new User() 访问实例属性、方法,通过 User.xxx 访问静态属性、方法。那么问题来了,这个 Useruser 究竟是什么类型?

class User {
  public name!: string;
  static smoking: string;

  public getName() {}
  static doSmoking() {}
}
const user: User = new User(); // 类也可以被当做类型。是它的实例类型
const staticUser: typeof User = User; // 通过 typeof User 获取 User 的类类型
// 当然除了 typeof User 还有其他的方案可以代替。这里先简单提一下,后续在高级类型中争取完善。
// 1,func: () => void  这里大家都知道他是一个函数类型。那么如果让他变成构造函数类型呢
// 2,new () => void 在前面加个关键字 new 他就变成了构造函数类型。此时在稍微改动一些参数,让他变成和 typeof User 同等的类型。
// 3,new (...arg: any) => User
const staticUser1: new (...arg: any) => User = User;
const staticUser2: { new (...arg: any): User } = User; // 或者也可以这样写。

类被当做类型使用

继上面所说,类既然可以被当做类型。那我们都可以做些什么呢?

首先我们要了解甭管语法怎么变编译之后类就是一个函数,可以被 new 的函数。接下来我们看下 new 都做些什么事情。

1,创建一个新的对象

2,将对象于构造函数的原型链连接起来

3,将构造函数中的 this 绑定到 新建的对象上

4,根据构造函数返回值类型做判断,如果是值类型忽略,如果是引用类型正常处理。

根据上面的内容推断,先把 new 之后的返回值看成一个对象没毛病吧?然后我们分别在 node 环境和 web 环境打印这个返回值看会得到什么。(浏览器也是支持 class 语法的)兼容性查询

class User {
  name;
  age;
  // 在浏览器环境下运行时把类型注解去掉即可。
  constructor(name: any, age: any) {
    this.name = name;
    this.age = age;
  }
  getName() {}
  getAge() {}
}
console.log(new User('zhangsan', 24)); // User { name: 'zhangsan', age: 24 }
console.log(User.prototype); // User { getName: [Function], getAge: [Function] }
// web 环境打印如下
// 1, User {name: 'zhangsan', age: 24}
// 2, {constructor: ƒ, getName: ƒ, getAge: ƒ}

根据以上推出其实 class 在 JS 中就是一个对象。再加上都知道实例方法其实是在原型中存放,结合原型链;尝试把类类型给一个对象会是什么结果呢?

const user: User = {}; // 类型“{}”缺少类型“User”中的以下属性: name, age, getName, getAge

注意: 在 TS 中被 protected private 也会被推断成类型的。这里顺便提一句 抽象类 也是可以被当做类型使用的。

接口 (interface)

TS 核心原则之一,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

个人总结:通过特定的语法来定义一些规则描述对象的类型。

// 2, 用接口来描述下面对象可以这样。此时 IUser 就是一个类型
interface IUser {
  name: string;
  age: number;
  eat(): void;
}
let user: IUser; // 3, 此时我们可以给这个 user 指定类型,让 user 的实现符合接口规则。
// 1, 假设我们碰到下面的对象。
user = {
  name: 'zhangsan',
  age: 24,
  eat() {
    console.log(this.name, '恰饭~');
  }
};

接口可选属性

interface IUser {
  addr?: string; // ? 表示可选属性
}

接口只读属性

interface IUser {
  readonly message: string; // readonly 只读属性
}
const user: IUser = { message: 'hello' };
user.message = 'hi'; // 无法分配到 "message" ,因为它是只读属性。

接口任意属性

需求:回到开头的地方,我们不希望这个接口只有 nameage 属性,后续想开放更多的属性。但是目前又不确定属性名怎么办呢?

interface IUser {
  user: string;
  age: number;
  eat(): void;
  [propName: string]: any;
}
const user: IUser = {
  user: 'zhangsan',
  age: 24,
  eat() {
    console.log(this.name, '恰饭');
  },
  message: '' // 此时我们可以定义任意的其他属性或者方法,但同时会跳出类型检查。
};

接口函数类型

interface IUser {
  eat(food: string): void;
  say: (chinese: string) => void;
}
const user: IUser = {
  eat: function (): void {
    throw new Error('Function not implemented.');
  },
  say: function (): void {
    throw new Error('Function not implemented.');
  }
};
// 函数接口声明
interface IFunc {
  (msg: string): void;
}
const func: IFunc = () => {};

// 当类型被定义时,肯定会有疑问为什么接口中的函数有参数,定义时没有也没有问题。
// 事实上不是没有问题。只是还没有出现。
user.eat(); // 应有 1 个参数,但获得 0 个。
func(); // 应有 1 个参数,但获得 0 个。

接口继承

一个接口可以继承另一个接口或者多个接口。

interface Person {
  eat(): void;
  say(): void;
}
interface BadBoy {
  smoking(): void;
}
interface User1 extends Person {} // 继承单个接口
// 继承多个接口
interface User extends Person, BadBoy {
  name: string;
  age: number;
}
// 此时的 user 应该包含上述接口中所有的属性和方法
const user: User = {}; // 类型“{}”缺少类型“User”中的以下属性: name, age, eat, say, smoking

接口类类型

从上面在 “类” 章节中提到类就是一个对象的概念。由此推断接口可以描述一个对象的形象,自然也是可以描述一个类的形象的。

interface IPerson {
  name: string;
  age: number;
  eat(): void;
}
// 当使用了 implements 关键字后并没有实现这个类时, Error: 类型“User”缺少类型“IPerson”中的以下属性: name, age, eat
// 类只能去实现接口并不能去 extends 继承接口。
class User implements IPerson {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  eat(): void {}
}

const user: IPerson = new User('zhangsan', 24); // 由于 User 实现了 IPerson 这个接口,IPerson 也是可以作为 user 的类型来使用。

ok,上面成功描述了类的实例对象的类型,并且去实现了这个类。接下来看下如何去描述类的静态部分,也就是类本身。

这个时候很聪明的小明,可能会想到在上个章节看到的 new (...arg:any): void 构造函数类型。兴致匆匆的做出以下实验。

interface IConstructor {
  new (): void;
}
class User implements IConstructor {
  // 类型“User”提供的内容与签名“new (): void”不匹配
  constructor() {}
}

看着很不合理为什么会失败?当类去实现一个接口的时候,只会对其实例部分进行检查。constructor 属于静态部分,不在类型检查的范围内。

如何规避以上场景,并且使用到该类型呢?假设我们现在需要一个工厂函数,传入对象我们返回对象的实例。此时可以这样做。

interface IConstructor {
  new (): void;
}
class User {
  name!: string;
  age!: number;
}
function createInstanceFactory(instance: IConstructor) {
  return new instance();
}
const instance = createInstanceFactory(User); // 小问题: instance 类型为什么是 void ?

接口继承类

既然类可以被当做类型使用,接口也可以去继承类类型

class User {
  name!: string;
  age!: number;
  protected sex!: string;
  private smoking!: string;
}

interface IPerson extends User {
  say(): void;
}
// 继承后的类型包含所有父类型和子类型的所有类型。
const user: IPerson = {}; // Error 类型“{}”缺少类型“IPerson”中的以下属性: say, name, age, sex, smoking

高级类型

类型断言

在某种情况下我们可能明确的知道这个值的类型,但是静态检查不是想要的类型。此时可以使用类型断言确定这个值的类型。

const arr = [1, '2', true];
// 此时数组下标 1 命名是字符串类型 可是我们却不能使用 length 属性
arr[1].length; // Error 类型“string | number | boolean”上不存在属性“length”

// 使用类型断言解决此问题
(arr[1] as string).length; // 第一种断言方式
(<string>arr[1]).length; // 第二种断言方式,请牢记格式不要在后面泛型的时候搞糊涂。

注意: 到目前共书写过两次 <> 语法;

  • Array<类型> : 数组泛型。

  • <类型>value : 类型断言。

联合类型

表示一个类型可以是多种类型中的一种。如果是复杂的类型比如 interface 则只能访问两种类型的共有属性。 如果两种类型中的相同属性的类型不同则该属性的类型为联合类型。

let name: string | number;
name = 1;
name = '1';

interface IUser {
  name: string;
  age: string;
  smoking: string;
}
interface IUser1 {
  name: string;
  age: number;
}
let user: IUser | IUser1; // 此时的 user 就只能访问共有属性。name, age。并且 age 的类型为 string | number

交叉类型

表示将两种类型合并为一个类型。如果两个类型中属性相同并且类型不同,则该属性必须满足这两种属性

interface IPerson {
  name: string;
}
interface IUser {
  name: number;
  age: number;
  smoking: string;
}
// 根据错误信息 name 的类型为 never 原因是 它既要满足 string 又要 满足 number 显然这样的类型是不存在的。
let user: IPerson & IUser = { name: 'zhangsan', age: 24, smoking: '' }; // Error 不能将类型“string”分配给类型“never”

类型推导

这是个理论性知识,TS 会根据书写的代码,自动推导出类型,但有些时候也并不会满足需求。

let num = 1; // 虽然没有类型注解,但是 num 仍然是 number 类型。
let arr = [1, 'string', true]; // (string | number | boolean)[] 此时被推导成为联合类型数组。

类型保护

在静态类型检查的过程中,收窄变量的类型,达到预期的目标。

typeof 类型保护

在上面类的章节,使用过 typeof User 类似的语法用户判定类构造函数的类型。但同时 typeof 也是 JS 内部的关键字。可以用来判定目标变量的类型。

// 通过 typeof 把类型确定为 string 或者 number 方便后续的操作。
function getUser(val: string | number) {
  if (typeof val === 'string') {
    // 业务逻辑。。。
  }
  if (typeof val === 'number') {
    // 业务逻辑。。。
  }
}

instanceof 类型保护

既然 typeof 可以收窄类型,那么 instanceof 必然也是可行的。

function execCallback(val: any) {
  if (val instanceof Function) {
    val();
  }
  console.log('fail~');
}
execCallback(() => console.log('success~')); // success~
execCallback('haha~'); // fial~

in 类型保护

判定一个值或者 key 是否存在于另一个变量中

const arr = [1, '2', true];
const obj = { name: 'zhangsan', age: 24 };

if (1 in arr) {
  console.log('success'); // 用于数组时,需要特定的值
}
if ('name' in obj) {
  console.log('success'); // 用于对象或者接口等 需要是 key
}

自定义类型保护 (is)

特定的格式,需要体现在函数的返回值中。注意 is 前面的参数必须来源于函数的参数。

function isObject(val: any): val is object {
  return typeof val === 'object' && val !== null;
}
console.log(isObject(null)); // fasle
console.log(isObject({})); // true

类型别名

可以给类型取个新的名字。可以用于声明大部分类型。

type funcType = (name: string, age: number) => void; // 函数类型
type UserType = { name: string; age: number }; // 对象类型
type strType = string; // 值类型
let func: funcType = function (name: string, age: number): void {};

类型别名 和 接口

表面看类型别名和接口差不多,但还是存在一些细微的差别。

  • 类型别名可以直接声明值类型。

  • 类型别名不能使用 extends implements 语法,但是接口可以。

  • 类型别名不能重名,接口重名会触发继承效果。

字面量类型

在 TS 中字面量不仅可以表示一个值,还可以作为类型使用。

type StrType = 'zhangsan';
let str: StrType = 'zhangsan'; // 此时 str 的类型就是 'zhangsan' 值也必须是 'zhangsan'

this 类型

思考一个问题,为什么 Promise 中的 then 方法可以无限被链式调用?

假设我们现在实现一个计算器类也想让他一直计算可以被链式调用。当然 this 类型不止这里可以使用还可以在其他地方比如 interface 中。至于指向的问题就不在细说了。

class Calc {
  public val: number = 0;
  add(val: number): this {
    this.val += val;
    return this;
  }
}
new Calc().add(1).add(2).add(3).add(4);

泛型

泛型 TS 的特性之一,个人认为也是比较难理解的点(起码对于我这种半路出家在此之前没有接触过强类型语言的人),水平有限也不能总结出比较优秀的心得。

官方说:我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。不仅要支持现有的数据类型,同时也要支持未来的数据类型,这样在构建大型项目时提供非常灵活的功能。

现在我们来实现一个简单的函数

function identity(value: number): number {
  return value;
}

目前通用性并不强,它只是支持 number 类型。或者可以使用 any 使函数变的更加通用。

function identity(value: any): any {
  return value;
}

现在问题出现了,虽然使用 any 能够解决此问题,但是就舍弃了使用 TS 的初衷。同时也丢弃了类型系统。切入正题,来定义第一个泛型函数。

function identity<T>(value: T): T {
  return value;
}
// 在调用的时候传入类型。
identity<number>(1);

1.4w字总结带你重学TypeScriptTypeScript是JavaScript类型的超集,它可以编译成纯JavaS

<T> 我们可以把他当做一个占位符,其中 T 代表的是类型变量,当调用函数的时候再传入对应的类型。

identity<number>(1) 有没有发现和类型断言很像?注意:类型断言的<类型>放到值的前面。

那现在就看一下上面函数执行的过程。

1.4w字总结带你重学TypeScriptTypeScript是JavaScript类型的超集,它可以编译成纯JavaS

为什么是 T ? 难道不能是其他的字母或者单词吗?当然可以 可以使任意单词或者字母,只要语义化清晰即可。建议首字母大写。当然 T 并不是毫无道理。

  • T : 代表 Type 在定义泛型时通常用作第一个类型变量名称.

  • K : 表示对象中的 key 类型。

  • V : 表示对象中的 value 类型。

  • E : 表示 Element 元素类型。

当然也可以定义多个泛型参数。

function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}
// 调用时同样要传入两个类型。
identity<number, string>(1, 'message');

1.4w字总结带你重学TypeScriptTypeScript是JavaScript类型的超集,它可以编译成纯JavaS

当调用 identity 时也可以不传递类型,可以依靠类型推导,推导出返回值的类型。(何时传递何时不传递?我只能说目前我能力有限不知道,多写多练。)

泛型默认类型

当使用泛型时可以给其指定默认类型,这样当调用时没有指定类型就会使用默认类型。

function identity<T = string, U = number>(value: T, message: U): T {
  console.log(message);
  return value;
}
identity('', 2);

泛型类型别名

经过上面对泛型的了解,下面就研究一下泛型相关的语法以及特性。

// 泛型函数类型的两种定义方式。
type identityType<T, U> = (value: T, message: U) => T;
type identityType1<T, U> = { (value: T, message: U): T };

// 使用泛型
let identity: identityType<string, number> = function (value, message) {
  return value;
};

// 对象泛型的声明方式
type userType<T> = { name: T; age: number };

// 使用泛型
let user: userType<string> = { name: 'zhangsan', age: 24 };

泛型接口

个人理解:泛型接口包含两类,一种是接口使用泛型,一级一级的向下传递类型;第二种则是单独给某个属性定义泛型。只有调用该属性时才去指定类型。

// 第一种方式 推荐。
interface IUser<T, U> {
  name: T;
  eat(food: U): U;
}
const user: IUser<string, string> = {
  name: 'zhangsan',
  eat(value) {
    return value;
  }
};
console.log(user.eat('xxx'));

// 第二种方式 注意:在内部使用泛型 仅限于方法。
interface IPerson {
  name: string;
  eat: <T>(food: T) => T;
}
const person: IPerson = {
  name: 'zhangsan',
  eat(value) {
    // 此时会碰到问题,value失去了类型提示。但是又不能指定类型
    return value;
  }
};
console.log(person.eat<number>(1314));

上面用泛型接口演示了如何描述对象。下面来看下怎样描述函数。

// 第一种方式
interface IPerson {
  <T>(name: T): T;
}
const person: IPerson = function (value) {
  // 此时会碰到问题,value失去了类型提示。但是又不能指定类型
  return value;
};
person<string>('zhangsan');
// 第二种方式 推荐
interface IUser<T> {
  (name: T): T;
}
const user: IUser<string> = function (value) {
  return value;
};
user('zhangsan');

泛型类

假设现在我们封装一个数组的增删改查类。(主要了解泛型类如何工作和编写)

class ArrayList<T> {
  public list: T[] = [];
  add(val: T) {
    this.list.push(val);
  }
  del(index: number) {
    this.list.splice(index, 1);
  }
  set(index: number, val: T) {
    this.list[index] = val;
  }
  get(index: number): T {
    return this.list[index];
  }
}
// 实例化的时候给定类型。
const list = new ArrayList<number>();
list.add(1);
list.set(0, 2);
console.log(list.get(0));
list.del(0);

泛型类类型

还记得前面的在 “接口类类型” 中提到的一个工厂函数中存在一个问题。下面是之前的问题代码。

interface IConstructor {
  new (): void;
}
class User {
  public name!: string;
  public age!: number;
}
function createInstanceFactory(instance: IConstructor) {
  return new instance();
}
const instance = createInstanceFactory(User); // 把鼠标放到 instance 上面发现是 void 类型。很明显不合理。

通过泛型可以使这个工厂函数更加完美。

// 注意: 当在函数参数中书写类类型的时候需要 { new (): T } 写法。
function createInstanceFactory<T>(instance: { new (): T }): T {
  return new instance();
}
const instance = createInstanceFactory<User>(User); // 此时我们的 instance 就是一个有类型的实例了。

泛型约束

有时候我们需要操作泛型参数,但事实上参数的类型只有在调用的时候才会传入。这也就造成了我们在传入之前操作会出现一些错误。

function getLength<T>(value: T): number {
  return value.length; // Error 类型“T”上不存在属性“length”
}

此时的解决方案可以是类型断言,结合现在的主题来尝试使用一下泛型约束。

interface ILength {
  length: number;
}
function getLength<T extends ILength>(value: T): number {
  return value.length;
}

如果需要约束多个条件 <T extends Type1, Type2, ...>

注意: 这里再次出现 extends 关键字,此处表示泛型必须包含某种类型,从而在调用时约束了传入的类型。

内置关键字以及高级类型

泛型几乎存在于 TS 的每一个角落,加上各种特殊的加持,从这里开始慢慢的难理解了 😭。

关键字 (typeof)

typeof 不仅可以用来判断类型,也可以推导出类型。注意: 千万不要按照 JS 中的 typeof 去理解。

class User {}
const obj = { name: 'zhangsan', age: 24 };
const str = '';
const num = 0;
const bool = true;
const n = null;
// 注意不要想着简写 type strType = typeof '';  Error 语法错误。
type nType = typeof n; // null 类型
type UserType = typeof User; // 构造函数类型。
type objType = typeof obj; // { name: string, age: number }
type strType = typeof str; // 空字符串 字面量类型
type numType = typeof num; // 0 字面量类型
type boolType = typeof bool; // true 字面量类型

关键字 (keyof) 索引类型查询操作符

此操作符会迭代被操作对象,取出其中的 key 作为字面量联合类型。查找的方式会进入原型,但是被 static protected private 修饰的属性或者方法不会被查找。需要注意的是像 null undefined 这些类型会推断出来 never,因为他只是一个值类型。

注意: 接口继承类的时候是所有的属性和方法都会被继承的。

class User {
  name!: string;
  protected age!: number;
  private smoking!: string;
  static sex: string;
  eat() {}
}

type nType = keyof undefined; // never
type UserType = keyof User; // 'name' | 'eat'
type objType = keyof { name: string; age: number }; // 'name' | 'age'
type strType = keyof string; // 'toString' | 'charAt' | ...
type numType = keyof number; // 'toString' | 'toFixed' | ...
type boolType = keyof boolean; // 'valueOf'

索引类型

在了解索引类型之前,先看一个 索引访问。假设现在有以下类型,我们想知道某个属性的类型。

type objType = { name: string; age: number };
// T[K] 此种访问方式被称为索引访问。
type nameType = objType['name']; // string

个人理解:索引类型其实就是 索引类型查询索引访问 的结合。

假设有需求,实现一个函数,该函数接收一个对象和数组,可以根据数组中的 key 来获取对象中对应 keyvalue

function getValue(obj: object, arr: string[]): any[] {
  // 可以看出用到了类型断言,来防止语法报错。
  return arr.map(item => (obj as any)[item]);
}

const obj = { name: 'zhangsan', age: 24 };
const arr = ['name', 'age'];
console.log(getValue(obj, arr)); // [ 'zhangsan', 24 ]

虽然实现了功能,但是并不合理,也不通用。

function getValue<T, K extends keyof T>(obj: T, arr: K[]): T[K][] {
  return arr.map(item => obj[item]);
}
const obj = { name: 'zhangsan', age: 24 };
const arr = ['name', 'age'];

console.log(getValue(obj, arr)); // Error 类型“string[]”的参数不能赋给类型“("name" | "age")[]”的参数

拆分理解以上代码

  • <T, K extends keyof T>keyof T 获取 T 类型的字面量联合类型,K extends 表示 K 必须来自 T 类型的字面量联合类型。

  • T[K][] : 相比较 T[K] 而言 obj['name'] 更容易理解。最后一个 [] 表示数组。

  • 注意错误的信息。函数参数中需要 ("name" | "age")[] 类型。arr 变量却是 string[]。都是类型推导的锅。

解决方案如下:

// 方案一
type arrType = keyof typeof obj; // 通过 typeof 获取类型,再通过 keyof 获取字面量类型
const arr: arrType = ['name', 'age'];

// 方案二
getValue(obj, ['name', 'age']); // 直接传递值会被判定为符合该类型需求。

映射类型 (in)

在某些情况下我们可能需要把某些旧类型变成我们需要的新类型。

interface User {
  name: string;
  age: number;
}
// 如果我们想把 User 变成可以变成可选类型,重写一次显然不合适。
// 此时可以使用 in 操作符来将 User 变成可选类型。此处可以按照 for...in 遍历 理解
type UserReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

有条件类型

infer 之前可以先了解一下有条件类型。T extends U ? X : Y 从语法的格式来看,能不能按照三元表达式来理解呢?答案是肯定可以的。

type typeName<T> = T extends string ? 'string' : T extends number ? 'number' : never;

type T0 = typeName<string>; // string
type T1 = typeName<null>; // never
// 分布式条件类型
type T2 = typeName<string | number>; // 'string' | 'number' 内部类似于迭代了这个联合类型逐个解析最终变成联合类型。

提到了循环,似乎可以尝试去比较两个联合类型。

type typeName<T, U> = T extends U ? T : never;

type unioType = typeName<'a' | 'b' | 'd', 'a' | 'b' | 'c'>; // 'a' | 'b' 字面量类型。不要混淆成值。

为什么是 'a' | 'b' 联合类型?

  • 表达式 T 存在于 U 返回 T

  • T 进行迭代 'a''b' 分别存在于 U 中,直接返回 d 不存在也就不处理了。

再比如我们想要获取一个接口类型中的函数名,属性名?

interface IUser {
  name: string;
  age: number;
  eat(): void;
}
type FunctionPropertys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type T0 = FunctionPropertys<IUser>; // 'eat' 字面量类型

😂 内容逐渐难以接受了,继续拆分看看是如何得到 'eat' 类型的。

  • { [K in keyof T]: T[K] extends Function ? K : never } 这是一个对象。

  • [K in keyof T]K 必须是 name age eat 之一。

  • T[K] extends Function ? K : never:取值必须是 Function 类型。

// 经过运算得到一下类型。
type T0 = { name: never; age: never; eat: 'eat' };

// 第四步解析  索引取值
type T1 = T0[keyof T0]; // 'eat' 字面量类型。

同理如果要获取属性名应该怎么写呢?自行脑补。

推断类型 (infer)

有条件类型中允许出现 infer 推断类型。它会引入一个待推断的变量,这个推断类型可以在有条件类型中的 true 分支中被引用,允许出现多个同类型变量的 infer

假设我们要获取一个函数的参数类型。可以这样做

type ParamType<T> = T extends (arg: infer P) => void ? P : never;
type FunctionType = ParamType<(arg: string) => void>; // string

// 如果要获取返回值类型
type ReturnType<T> = T extends (...arg: any[]) => infer R ? R : any;

infer 被放到协变的位置上,同一个变量上存在多个类型会被推断为联合类型。

type CovariantType<T> = T extends { a: infer K; b: infer K } ? K : never;
type T0 = CovariantType<{ a: string; b: number }>; // string | number

协变: TS 类型兼容的一个话题名词,还有 逆变,双向协变,不变 。又是一个沉痛的话题 😥

内置工具类型

TS 提供一些工具类型来帮助常见的类型转换,这些类型是全局可用。在使用时可以直接调用,Contrl + 鼠标 可直接查看相关源码。

Partial

返回一个新的全部属性可选的类型。

/**
 * Make all properties in T optional
 * 将 T 中的所有属性设为可选
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};
  • 通过 keyof T 获取 T 的字面量联合类型;

  • 通过 in 遍历这个类型让 P 变成 T 的 key;

  • 通过 T[P] 取出原来 T 中的 value;

  • ?: 表示可选属性。

示例

interface User {
  name: string;
  age: number;
}
type PartialUser = Partial<User>; // { name?: string, age?: number }

Required

返回一个新的全部属性必选的类型。

/**
 * Make all properties in T required
 * 使 T 中的所有属性成为必需
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};
  • -? 表示移除可选属性。

示例

interface User {
  name?: string;
  age?: number;
}
type RequiredUser = Required<User>; // { name: string, age: number }

Readonly

返回一个新的全部属性只读的类型。

/**
 * Make all properties in T readonly
 * 将 T 中的所有属性设为只读
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

示例

interface User {
  name: string;
  age: number;
}
type ReadonlyUser = Readonly<User>; // { readonly name: string, readonly age: number }

Readonly 是一个工具类型,也可以使用断言来代替它的工作 as const 表示该变量为一个只读变量。

const user = { name: 'zhangsan', age: 24 } as const; // 只读对象
const users = ["zhangsan", 'lisi'] as const; // readonly ["zhangsan", "lisi"]
const name = 'zhangsan' as const; // 对于值类型而言这个变量的类型就是这个值。
let n = null as const; // Error "const" 断言只能用于引用枚举成员、字符串、数字、布尔值、数组或对象文本

Pick

从一个对象中取出一些属性构造一个新的对象。

/**
 * From T, pick a set of properties whose keys are in the union K
 * 从 T 中选择一组属性,其键在并集 K 中
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

示例

interface User {
  name: string;
  age: number;
}
type PickUser = Pick<User, 'name'>; // { name: string }

Record

构造一个 key 为 string | number | symbol 类型,value 为任意类型的对象。

/**
 * Construct a type with a set of properties K of type T
 * 构造一个具有一组 T 类型的属性 K 的类型
 */
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
  • keyof any 这里比较容易引起误解,它的返回类型实际是 string | number | symbol 的联合类型。所以 P 是这三种类型之一。

示例

interface User {
  name: string;
  age: number;
}
// 传递第一个参数为 'name' | 'age' 的字面量联合类型
type UserRecord = Record<keyof User, User>; // { name: User, age: User }

Exclude

排除一个联合类型中的某一些类型。

/**
 * Exclude from T those types that are assignable to U
 * 从 T 中排除那些可分配给 U 的类型
 */
type Exclude<T, U> = T extends U ? never : T;

示例

type unioUser = 'name' | 'username';

// 排除 unioUser 类型中的 'name' 类型。
type UserExclude = Exclude<unioUser, 'name'>; // 'username'

Extract

从一个联合类型中提取某一些类型,刚好合 Exclude 相反。

/**
 * Extract from T those types that are assignable to U
 * 从 T 中提取可分配给 U 的那些类型
 */
type Extract<T, U> = T extends U ? T : never;

示例

type unioUser = 'name' | 'age' | 'sex';

// 获取 unioUser 中的 'name' | 'age' 类型
type UserExtract = Extract<unioUser, 'name' | 'age'>; // 'name' | 'age'

Omit

从一个对象中排除一些不需要的属性。刚好于 Pick 相反。

/**
 * Construct a type with the properties of T except for those in type K.
 * 构造一个具有 T 的属性的类型,除了类型 K 中的那些。
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

示例解析

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

// 1,先计算 Exlcude 的返回类型
type ExcludeType = Exclude<keyof IUser, 'name'>; // 'sex' | 'age'
// 2,再计算 Pick 的返回类型
type PickType = Pick<IUser, ExcludeType>; // { sex: string, age: number }

// 3,综合使用验算
type OmitUser = Omit<Iuser, 'name'>; // { sex: string, age: number }

模块

在 TS 中模块的概念 ES6 一样,任何包含顶级的 importexport 的文件都被当成一个模块。相反如果一个模块不带有 importexport 声明,那么它的内容是被视为全局可见的。

// a.ts
const str: string = '';
// b.ts
const str: string = ''; // Error 无法重新声明块范围变量“str”。

如果实在是没有内容导入导出又不想要这些错误可以使用以下操作

import ''; // 第一种
export {}; // 第二种

由于包含了关键字中的其中一种,该文件就被当做模块,其中的内容也只有模块内可以共享。

导入、导出声明。

如果想要在 b.ts 中使用这个 str 变量,此时可以将模块导出,此种导出的方式也被称为 导出声明 。任何类型的声明都可以通过此种方式导出。(比如变量,函数,类,类型别名,接口)

// a.ts
export const str: string = '';
// b.ts
import { str } from './a.ts';

导入、导出别名

可以给想要被导入、导出的内容起一个别名

// a.ts
const str: string = '';
export { str as Str }; // 此时可以通 as 关键字给被导出的内容重新命名。
// b.ts
import { Str as str } from './a.ts';

重新导入、导出

通过导出语句,现在拥有了 Str 变量,如果我们想要 Str 变成 b.ts 的功能并且供外部使用。

// b.ts
export const Str: string = '';
export { Str as fromA } from './modules'; // 把 a.ts 中的 Str 换个名字再暴露出去

// 等价于
import { Str } from './modules';
export { Str as fromA };

或者一个模块可以包含多个模块合并后统一进行导出。

假设我们现在有 strUtil numUtil arrUtil 等一系列的工具方法,其中每个类型都是一个单独的模块。如果需要统一暴露可以这样做。

// util/index.ts
export * from './strUtil';
export * from './numUtil';
export * from './arrUtil';

// 那么如果在外部使用时应该是这种导出方式。
import * as util from './util/index.ts';

默认导入、导出

每个模块都能有一个 default 导出,并且只能有一个。

export default '默认导出';

// 默认导入 不是很推荐的方法,但有时候也会需要。
import './index.ts';

命名空间

总所周知在 ES6 之前如果为了防止变量污染可以使用立即执行函数,在 ES6 有了块级作用域可以在 {} 内部使用变量防止污染。在 TS 中也提供了类似的功能。

namespace Util {
  const phone = /xxxx/;
  const email = /yyyy/; // 没有 export 外部是没有办法访问的。
  export const validatorPhone = function () {};
  export const validatorEmail = function () {};
}
Util.validatorPhone();
Util.validatorEmail();

装饰器

在某些特定的情况下可能需要额外修改类及其成员,装饰器可以在声明以及成员上通过元编程语法添加标注提供了一种方式。需要注意的是,这是一种实验性质的功能,在使用之前需要打开对应 tsconfig.json 中的一些配置。

{
  // ...
  "experimentalDecorators": true
  // ...
}
  • 装饰器是一种特殊类型的声明,它能附加在类声明,方法,访问符,参数,属性上。

  • 装饰器使用 @methodName 的形式。methodName 必须为一个函数。

  • 装饰器有两种写法,分别为装饰器以及装饰器工厂。

  • 多个装饰器可以在同一声明上使用。

类装饰器

类装饰器应用于构造函数,可以用来监视,修改和替换类含义。它接收一个参数为类的构造函数

function classDecorator(target: new (...arg: any) => any) {
  console.log(target); // [Function: User]
}

@classDecorator
class User {}

假设我们现在想在类被实例化的时候打印一个 log,此时可以重载这个类。

function classDecorator(target: new (...arg: any) => any) {
  return class extends target {
    constructor() {
      super();
      console.log(`INFO : User 被实例化了。。。`);
    }
  };
}

方法装饰器

方法装饰器可以用来监视,修改或者替换方法定义。它接收三个参数

  • target 对于静态成员是类的构造函数,对于实例成员是类的原型对象

  • key 方法的名字

  • descriptor 成员的属性描述符

function methodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  // User { getUser: [Function] }
  console.log(target);
  // getUser
  console.log(key);
  // { value: [Function],
  //   writable: true,
  //   enumerable: true,
  //   configurable: true }
  console.log(descriptor);
}
class User {
  @methodDecorator
  getUser() {}
}

此时如果使用 for...in 语法会把 getUser 给循环出来,如果我们不想这么做,可以使用装饰器工厂更改 descriptor

function enumerable(value: boolean): MethodDecorator {
  return function (target, key, descriptor) {
    descriptor.enumerable = value;
  };
}

class User {
  @enumerable(false)
  getUser() {}
}

当然也可以重写这个方法。这一切都要归功于 descriptor 参数。

function userLog(target: Object, key: string, descriptor: PropertyDescriptor) {
  let oldMethod = descriptor.value;
  descriptor.value = function (...arg: any) {
    console.log('即将查询用户。。。');
    oldMethod.apply(this, arg);
    console.log('用户查询结束。。。');
  };
}
class User {
  @userLog
  getUser() {
    console.log('正在查询用户。。。');
  }
}

任一声明中都可以使用多个装饰器。

可以在一行中

@userLog @enmuerable(false)
getUser() {}

可以多行中声明

@userLog
@enmuerable(false)
getUser() {}

同一声明中的装饰器执行顺序为 末尾或者最靠近声明的装饰器优先执行。

属性装饰器

  • target 对于静态成员是类的构造函数,对于实例成员是类的原型对象

  • key 属性的名字

注意:属性装饰器没有接收属性描述符,TS 归根也是 JS 的超集,最终都会编译成 JS 代码,目前没有办法在定义一个原型对象的时描述一个实例属性。并且没有办法监视或修改一个属性的初始化方法,返回值也会被忽略。

怎么理解?我们可以尝试把代码用 tsc 命令进行编译,目标为 es5。可以看到一个 __decorate 函数,这就是装饰器的核心实现。里面有个很重要的方法 getOwnPropertyDescriptor 我们可以尝试一下。

class User {
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  getName() {}
}
console.log(Object.getOwnPropertyDescriptor(User.prototype, 'name')); // undefined
console.log(Object.getOwnPropertyDescriptor(User.propotype, 'getName')); // { value: [Function], writable: true, enumerable: true, configurable: true }

正是因为以上的特性,注定了属性装饰器能做的事情少的可怜。最常见的应该就是注入元数据。在使用之前需要安装 reflect-metadata

import 'reflect-metadata';

function attribute(value: string): PropertyDecorator {
  return function (target, key) {
    // 在User的prototype上面定义一个 key为 metadata-key 数据为 value 的元数据
    Reflect.defineMetadata('metadata-key', value, target);
  };
}
class User {
  @attribute('Hello, %s')
  greeting: string;
  constructor(_greeting: string) {
    this.greeting = _greeting;
  }
  greet() {
    // 从User的prototype上面获取一个 key 为 metadata-key 的元数据
    const metaData = Reflect.getMetadata('metadata-key', this);
    console.log(metaData.replace('%s', this.greeting));
  }
}
const user: User = new User('zhangsan');
user.greet(); // Hello zhangsan

参数装饰器

参数装饰器应用于类构造方法或者方法声明,参数装饰器只能用来监听一个方法的参数是否被传入。接收三个参数。

  • target 对于静态成员是类的构造函数,对于实例成员是类的原型对象

  • key 方法的名字

  • index 参数在所在方法的索引。

function required(target: Object, key: string, index: number) {
  let requiredParams: number[] = [];
  requiredParams.push(index);
  // 在 target(User原型) 的 key (方法名)上面定义一个 index 数组元数据
  Reflect.defineMetadata('param-key', requiredParams, target, key);
}

function validator(target: Object, key: string, descriptor: PropertyDescriptor) {
  // 从 target(User原型) 的 key (方法名) 获取元数据
  const requiredParams: number[] = Reflect.getMetadata('param-key', target, key);
  let oldMethod = descriptor.value;
  descriptor.value = function () {
    let arg = arguments;
    requiredParams.forEach(function (index) {
      if (arg[index] === undefined) {
        throw new Error('参数不能为空');
      }
    });
    oldMethod.apply(this, arg);
  };
}

class User {
  @validator
  eat(@required food?: string) {
    console.log('吃', food);
  }
}
const user: User = new User();
user.eat(); // Error

访问器装饰器

访问器装饰器可以用于访问器的属性描述符,并且可以用来监视,修改或者替换一个访问器的定义,总所周知访问器是有 getset 两部分。当使用装饰器装饰访问器时,只能在文档顺序中第一个装饰器中。

class User {
  @getter
  get _name() {}
  @getter // Error 不能向多个同名的 get/set 访问器应用修饰器
  set _name(val) {}
}

访问器装饰器接收三个参数

  • target 对于静态成员是类的构造函数,对于实例成员是类的原型对象

  • key 访问器的名字

  • descriptor 成员的属性描述符

function configurable(value: boolean): MethodDecorator {
  return function (target, key, descriptor) {
    descriptor.configurable = value;
  };
}

class User {
  private name: string;
  constructor(_name: string) {
    this.name = _name;
  }

  @configurable(false)
  get _name() {
    return this.name;
  }
}

装饰器的执行顺序

上面在方法装饰器中提到 同一声明下的装饰器的执行顺序为:同一声明中的装饰器执行顺序为 末尾或者最靠近声明的装饰器优先执行。 那么如果存在多个不同的装饰器执行顺序又当如何呢?

function classDescriptor(target: { new (...arg: any): any }) {
  console.log('类装饰器', 5);
}
function attributeDescriptor(target: Object, key: string | symbol) {
  console.log('属性装饰器', 1);
}
function methodDescriptor(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
  console.log('方法装饰器', 3);
}
function paramDescriptor(target: Object, key: string | symbol, index: number) {
  console.log('参数装饰器', 2);
}
function getterDescriptor(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
  console.log('访问器装饰器', '取决于书写的位置但总是会优先于类装饰器执行', 4);
}

@classDescriptor
class User {
  @attributeDescriptor
  name!: string;

  @methodDescriptor
  getName(@paramDescriptor id: number) {}

  @getterDescriptor
  get _name() {
    return this.name;
  }
}
// 属性装饰器 1
// 参数装饰器 2
// 方法装饰器 3
// 访问器装饰器 取决于书写的位置但总是会优先于类装饰器执行 4
// 类装饰器 5

元数据

Reflect Metadata 是 ES7 的提案,主要用于对元数据的一系列操作。在 TS 中使用元数据需要 npm install reflect-metadata 并且在 tsconfig.json 中放开相关配置。

{
  // ...
  "emitDecoratorMetadata": true
  // ...
}

元数据:描述数据的数据,对数据以及信息资源的描述信息。在软件领域元数据不是被加工的数据对象,而是通过其值的改变来改变的行为的数据。

概念总是枯燥乏味且晦涩难懂,其实就是内部维护一个全局的 _WeakMap 来进行数据的操作。至于为什么不是原始的 WeakMap 对象,这只能说 API 比较高级需要 Pollyfill。

const Metadata = new _WeakMap<any, Map<string | symbol | undefined, Map<any, any>>>();

相信看到这个泛型新手也是比较懵,分解一下

  • WeakMap 接收 key value 其中 value 是一个 Map

  • 第一个 Map 接收 key value 其中 value 又是一个 Map

  • 最后一个 Map 接收两个对象

let secondMap = { any: 'any' };
let firstMap = { string: secondMap };
let weakMap = { any: firstMap };

// 总结 是一个三维对象
weakMap = { any: { string: { any: 'any' } } };

虽然上面在装饰器的章节有使用到 metadata 中的功能,但需要注意的是这并不是和 装饰器强绑定的库。

defineMetadata 定义元数据

function defineMetadata(metadataKey: any, metadataValue: any, target: Object);
function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol);

比如给对象以及对象的属性定义一组元数据。

const obj = { name: '' };

Reflect.defineMetadata('obj', '这是定义到obj上面的元数据', obj);
Reflect.defineMetadata('name', '这是name上面定义的元数据', obj, obj.name);

getMetadata 获取元数据

function getMetadata(metadataKey: any, target: Object);
function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol);

比如获取上面定义的方法

console.log(Reflect.getMetadata('obj', obj));
console.log(Reflect.getMetadata('name', obj, obj.name));

hasMetadata 是否存在某个元数据

function hasMetadata(metadataKey: any, target: Object);
function hasMetadata(metadataKey: any, target: Object, propertyKey: string | symbol);

比如可以查看 obj 中是否存在 name 的元数据

console.log(Reflect.hasMatadata('name', obj)); // false
console.log(Reflect.hasMatadata('name', obj, obj.name)); // true

deleteMetadata 删除元数据

function deleteMetadata(metadataKey: any, target: Object);
function deleteMetadata(metadataKey: any, target: Object, propertyKey: string | symbol);

比如删除 objname 的元数据

console.log(Reflect.hasMetadata('name', obj, obj.name)); // true
Reflect.deleteMetadata('name', obj, obj.name);
console.log(Reflect.hasMetadata('name', obj, obj.name)); // false

metadata 定义元数据

基于 defineMetadata 进行封装,返回的是一个符合装饰器格式的函数。因此可以直接在类上进行使用。个人不太推荐直接在类上使用。

@Reflect.metadata('user', User)
class User {}

getMetadataKeys 获取原数据的所有 key

function method(target: Object, key: string, descriptor: PropertyDescriptor) {
  const keys = Reflect.getMetadataKeys(target, key);
  console.log(keys);
  // [ 'design:returntype',
  // 'design:paramtypes',
  // 'design:type',
  // 'name1',
  // 'name2',
  // 'name3' ]
}

class User {
  @method
  @Reflect.metadata('name3', '这是第三个元数据')
  @Reflect.metadata('name2', '这是第二个元数据')
  @Reflect.metadata('name1', '这是第一个元数据')
  getName(id: number, name: string): string {
    return '';
  }
}

根据上面的运行结果发现获取到的数据还多出了三个,分别是 design:returntype design:type design:paramtypes。这是 Reflect Metadata 内部提供的一些获取类型的方法。

  • design:returntype 获取返回类型。

  • design:paramtypes 获取参数类型,返回数组

  • design:type 返回被装饰的成员类型。比如装饰一个方法那就是 [Function: Function]

此时可以通过修改 method 装饰器验证上述问题。

function method(target: Object, key: string, descriptor: PropertyDescriptor) {
  console.log(Reflect.getMetadata('design:type', target, key)); // [Function: Function]
  console.log(Reflect.getMetadata('design:returntype', target, key)); // [Function: String]
  console.log(Reflect.getMetadata('design:paramtypes', target, key)); // [ [Function: Number], [Function: String] ]
}

Own 系列 API

其中包含 getOwnMetadata getOwnMetadataKeys hasOwnMetadata 这三个 API,功能和上面介绍到的功能类似。可以通过 own 来判定,获取自身的 metadata 相关信息。

怎么理解呢?假设我们现在有子类 Person 父类 People 此时在 Person 中定义的元数据 People 是访问不到的。

@Reflect.metadata('people', 'peopleData')
class People {}

function PersonDescriptor(target: { new (...arg: any): any }) {
  console.log(Reflect.getOwnMetadata('people', target)); // undefined
  console.log(Reflect.getMetadata('people', target)); // peopleData
}

@PersonDescriptor
@Reflect.metadata('person', 'personData')
class Person extends People {}

上述运行结果可以看出在子类中如果直接获取父类中定义的 metadata 可以获取到。但使用 Own 系列的 API 则返回为 undefined

tsconfig.json

如果一个目录下存在 tsconfig.json 文件,则代表这个目录为项目的根目录。当使用 tsc 命令编译项目时,会首先从根目录查找 tsconfig.json 文件中的配置。

默认情况下使用 tsc --init 默认生成的 tsconfig.json 里面所有的配置项都包含在 compilerOptions 选项下。与此同级还有一些配置选项,以下称为顶级属性。

顶级属性 files include exclude

  • files 指定一个相对或者绝对文件的列表进行编译。
{
  "compilerOptions": {},
  "files": ["./src/index.ts"] // 此时只会编译这个文件,其他的不会被编译
}
  • include 指定一个相对或者绝对文件的列表进行编译,可以使用路径模式匹配。
{
  "compilerOptions": {},
  "include": ["./src/index.ts"] // 此时只会编译这个文件,其他的不会被编译
}

如果同时指定了 filesinclude 选项会进行合并。

  • exclude 排除一个相对或者绝对文件的列表进行编译,可以使用路径模式匹配。
// 假设 src 为工作目录
{
  "compilerOptions": {},
  "exclude": ["./src/index.ts"] // 排除 src 下的 index.ts 文件不进行编译
}

如果指定了 includeexclude 的过滤仍然会生效。但是如果指定了 files 那么 exclude 的过滤无效。

exclude 默认情况下会排除 node_modules bower_components jspm_packages 还有 compilerOptionsoutDir 指定的目录。

路径配置模式

  • * 匹配 0 或者 多个字符,不包含目录分隔符 /

  • ? 匹配任意一个字符,不包含目录分隔符 /

  • **/ 递归匹配任意子目录。

示例:

{
  "exclude": ["./src/**/*"], // src目录下的任意目录下的任意文件
  "exclude": ["./src/**/*.log"], // src目录下任意目录下的以 .log 结尾的任意文件
  "exclude": ["./src/**/?.ts"] // src目录下任意目录下的以单个字母命名的 ts文件。
}

顶级属性 extends

tsconfig.json 可以通过 extends 从另一个文件中继承配置。在编译的过程中,会优先加载继承文件的配置,然后再被源文件的配置覆盖。如果出现循环引用则抛出异常。

// tsconfig/base.json
{
  "include": ["../src/modules/a.ts"] // 路径的书写以当前文件为准。
}
// tsconfig.json
{
  "inlcude": ["./src/index.ts"],
  "extends": "./tsconfig/base.json"
}

按照以上配置 最终 tsconfig.json 中的配置会生效。

顶级属性 compilerOptions

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true,         // 为装饰器提供元数据的支持

    /* 高级选项 */
    "skipLibCheck": true,                  // 跳过声明文件的类型检查
    "forceConsistentCasingInFileNames": true// 禁止对同一文件大小写不一致的引用
  }
}

文章到这里就算结束了,你能看到这里我是真心佩服。TS 本人也是学了一遍又一遍,都是得不到应用而最终还给了老师。希望大家都能早日成为类型体操运动员。当然也会有一些遗憾,比如 声明文件如何做?在 Vue 以及 React 中如何使用?TS 中的类型兼容性,类型的协变、逆变、双向协变、不变性?还不快去卷一卷?

转载自:https://juejin.cn/post/7096695346239111199
评论
请登录