TypeScript 史上最强学习指北

858 阅读17分钟

前言

随着前端生态的不断发展,太多的明星项目(Vue.js、React 等)如今都是使用 TypeScript 编写,TypeScript 重要性已经不言而喻了,对于前端童鞋来说学习掌握 Typescript 不仅仅是现在或者未来的需要,也是学习源码进阶更高阶职位的需要。

TypeScript 是什么

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

TypeScript 环境搭建

安装typescript

npm i -g typescript

安装 ts-node

npm i -g ts-node

初始化 Typescript

tsc --init

然后新建index.ts,输入相关代码,然后执行 ts-node index.ts

ps:官方也提供了一个在线开发 TypeScript 的云环境——Playground

TypeScript 基础类型

Ts 基础类型有:数值(Number)、字符串(String)、布尔(Boolean)、数组(Array)、枚举(Enum)、Any、元祖(Tuple)、VoidUnknownNeverNullUndefined

// number
let decLiteral: number = 6;
//string
let name: string = "bob";
//boolean
let isDone: boolean = false;
// array
let list: number[] = [1, 2, 3];
// enum
enum Color {Red, Green, Blue}
// any
let notSure: any = 4;
// tuple
let x: [string, number] = ['hello', 10];
// void
let unusable: void = undefined;
// unknown
let value: unknown;
value = true; // OK 
value = 42; // OK
value = "Hello World"; // OK 
value = []; // OK
// never
function error(message: string): never { throw new Error(message); }
// null和undefined
let u: undefined = undefined; 
let n: null = null;
  • 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其他类型,然而,如果你指定了--strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自的类型。
  • 所有类型也都可以赋值给 unknown
  • never 类型表示的是那些永不存在的值的类型。

TypeScript 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

尖括号 语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

类断言

  • 两个类(接口)是继承关系,可以互相断言。
  • 没有继承关系的类,两个类所有的公有的属性加起来一样的话,也可以互相断言。

类型守卫

定义:在语块作用域【if语句,条目运算符表达式】缩小变量的一种类型推断行为【typeof、in、instanceof】。

typeof 关键字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}
  • typeof的作用是用来检测一个变量或者一个对象的类型。它的检测范围主要有stringnumberbooleanundefinedbigintobjectfunctionsymbol
  • typeof也有一定的局限性--检测变量并不完全,例如typeof null 结果为object,或者typeof检测数组变量也为object,因为数组创建的本质是new的底层时创建一个对象变量。
  • 可以使用Object.prototype.toString.call()来解决typeof的局限性,但是这个替代方案依然无法解决的问题是无法获取自定义实例变量或构造函数的对象真正创建类型。只能用instanceof来替代。

instanceof 关键字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

in 关键字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

自定义守卫

格式:
function(str: any): str is string (= boolean + 类型守卫的能力) {
	return true or false
}

function isFunction (data: any): data is Function {
  return typeof data === 'function'
}
function add(fuc: any) {
  let value: unknown
  value = fuc
  if (isFunction(value)) {
    value()
  }
}

高级类型

联合类型

联合类型表示取值可以为多种类型中的一种,使用 | 分隔每个类型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("Semlinker");
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

交叉类型

TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

TypeScript 类

属性、方法、静态属性和静态方法

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:

class Greeter {
  // 静态属性
  static cname: string = "Greeter";
  // 成员属性
  greeting: string;

  // 构造函数 - 执行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }

  // 静态方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成员方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

其中属性和方法前面加上 static 关键字则变为静态属性和方法,静态属性和方法在类初始化之前就会开辟单独且唯一的出内存空间。

类修饰符

  • public: 公有 在类里面 子类 类外面都可以访问
  • protected: 保护类型 在类里面 子类里面可以访问
  • private: 私有 类里面可以访问
class Person{     
    public age:number = 20     
    protected name:string     
    private sex:string = 'male'      
    constructor(age:number, name:string, sex:string){  
        //构造函数 实例化类的时候触发的方法        
        this.name = name         
        this.age = age         
        this.sex = sex    
    }     
    getName():string {         
        return this.name     
    }     
    setName(name:string):void {        
        this.name = name    
    }     
    getAge():void {         
        console.log(this.age)   //类内访问    
    } 
} 
//继承 
class Web extends Person{     
    constructor(age:number, name:string, sex:string){         
        super(age, name, sex)     
    }     
    work(){         
        console.log(this.name)   //访问protected name         
        console.log(this.sex)  // 错 不能访问     
    }
} 
let p = new Person(20, 'li', 'male')
console.log(p.age) //访问public age 
// console.log(p.name) 错 
// console.log(p.sex)   错 
let w = new Web(30, 'wang', 'female') 
w.work() 

ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

类构造器

Ts 中类构造器参数前面加上修饰符,则参数会被当作属性:

class Person {
  // 感叹号是非null和非undefined的类型断言
  // TS4 之后  public count:number | undefined 
  public count!: number 
  constructor(public name: string, public age: number, public phone: string) {
    // public name name 变成属性
    // 相当于 this.name = name
  }
  public doEat(who: string, where: string): void {
    console.log(`${this.name}${who},在${where}吃饭`);
  }
  public doStep() {
    return this.count * this.count
  }
}

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。

class Point { 
  x: number; 
  y: number; 
  constructor(a: number, b: number) {
    this.x = a
    this.y = b
  }
} 
interface Point3d extends Point { 
    z: number; 
} 
let point3d: Point3d = {x: 1, y: 2, z: 3};

type T = typeof Point // new (a: number, b: number) => void

继承

class Animal {
  name: string;
  
  constructor(theName: string) {
    this.name = theName;
  }
  
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

多态

多态:父类定义一个方法不去实现,让继承他的子类去实现,每一个子类有不同的表现, 多态属于继承。

class Futher {
    public age: number;
    constructor(age: number) {
        this.age = age
    }
    counts(): void {
        console.log(this.age)
    }
}
class children1 extends Futher {
    constructor(age: number) {
        super(age)
    }
    counts(): void {    /* 多态,重写方法不执行父类方法 */
        console.log(this.age - 1)
    }
}
class children2 extends Futher {
    constructor(age: number) {
        super(age)
    }
    counts(): void {
        console.log(this.age + 1)
    }
}

抽象类

abstract定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。

  • 抽象方法必须在抽象类中。
  • 抽象类和抽象方法是一个标准,定义标准后,子类中必须包含抽象定义的方法。
abstract class Futher { /* 定义一个抽象方法 */
    public age: number;
    constructor(age: number) {
        this.age = age
    }
    abstract counts(): any; /* 抽象方法必须在抽象类中 */
}
class children extends Futher {
    constructor(age: number) {
        super(age)
    }
    counts(): void {    /* 子类中必须有抽象类中的抽象方法 */
        console.log(this.age - 1)
    }
}

访问器

在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

TypeScript 函数

参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id;
}

函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

构造签名和构造函数类型

构造签名:

在 TypeScript 接口中,你可以使用 new 关键字来描述一个构造函数:

interface Point {
  new (x: number, y: number): any;
}

以上接口中的 new (x: number, y: number) 我们称之为构造签名,其语法如下:

ConstructSignature:
`new` TypeParametersopt `(` ParameterListopt* `)` TypeAnnotationopt

在上述的构造签名中,TypeParametersoptParameterListoptTypeAnnotationopt 分别表示:可选的类型参数、可选的参数列表和可选的类型注解。与该语法相对应的几种常见的使用形式如下:

new C  
new C ( ... )  
new C < ... > ( ... )

构造函数类型:

  • 包含一个或多个构造签名的对象类型被称为构造函数类型;
  • 构造函数类型可以使用构造函数类型字面量或包含构造签名的对象类型字面量来编写。 构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:
new < T1, T2, ... > ( p1, p2, ... ) => R

该形式与以下对象字面量类型是等价的:

{ new < T1, T2, ... > ( p1, p2, ... ) : R }

下面我们来举个实际的示例:

// 构造函数字面量
let Constructor: new (x: number, y: number) => any

等价于以下对象类型字面量:

let Constructor: { new (x: number, y: number): any; }

可选参数和默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "Semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

剩余参数

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

TypeScript 重载

构造器重载

重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力,详细的定义查看下面函数重载内容。

class Square {
	public width: number
  public height: number
  constructor(_width: number, _height: number)
	constructor(params: type_ChartParams)
	constructor(valueOrWidth: number, _height?: number) {
  	if (typeof valueOrWidth === 'number) {
        }
    ...
  }
}

方法重载

方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
    if (typeof a === "string" || typeof b === "string") {
      return a.toString() + b.toString();
    }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add("Semlinker", " Kakuqo");

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

函数重载

函数重载定义:(包含以下规则的一组函数就是TS函数重载)

  • 由一个实现签名加一个或多个重载签名合成。(实现签名函数才有函数体,且只能有一个,重载签名可以多个)
  • 外部调用函数重载函数定义,只能调用重载签名,不能调用实现签名。TS规定:实现签名下的函数体是给重载签名编写的,实现签名只是定义时起到了统领所有重载签名的作用,在执行调用时就看不到实现签名了。
  • 调用重载函数时,会根据传递的参数来判断你调用的是哪个函数。
  • 只有一个函数体,只有实现签名配备函数体,所有重载签名只要签名没有函数体。
  • 实现签名的参数类型,和返回类型是所有重载签名的父集。 其中函数签名 = 函数名称+函数参数+函数参数类型+返回值类型四者合成。在Ts 函数重载中,包含实现签名和重载签名,两者都是函数签名。
// 重载签名
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
// 实现签名
function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。

泛型重载

function cross<T extends object, U extends object>(objOne: T, objTow: U): T & U
function cross<T extends object, U extends object, V extends object>(objOne: T, objTow: U, objThree: V): T & U & V
function cross<T extends object, U extends object, V extends object>(objOne: T, objTow: U, objThree?: V) {
  let obj = {}
  let combine = obj as T & U
  Object.keys(objOne).forEach((key) => {
    combine[key] = objOne[key]
  })
  Object.keys(objTow).forEach(key => {
    if (!combine.hasOwnproperty(key)) {
      combine[key] = objTow[key]
    }
  })
  if (objThree) {
    let combine2 = combine as typeof combine & V
    Object.keys(objThree).forEach(key => {
      if (!combine2.hasOwnproperty(key)) {
        combine[key] = objThree[key]
      }
    })
  }
  return combine
}

TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

可选 | 只读属性| 任意属性

interface Person {
  readonly name: string; // 只读
  age?: number; // 可选
  [propName: string]: any; // 任意属性
}
let tom: Person = { name: 'Tom', gender: 'male' };

属性接口

function printLabel(label:string):void {
    console.log('print')
}
function print(labelInfo: { label: string }):void {
    console.log('labelInfo')
}

// print('haha')    错误
// print({name: 'haha'})    错误
print({label: 'wang'})

//interface关键字定义接口
interface FullName {
    firstName: string
    secondName?: string //可选属性
}
function printName(name:FullName){  //必须传入对象firstName secondName
    console.log(name.firstName + name.secondName)
}
printName(20,'zhang','san') // 错 只能传两个字符串

函数类型接口

//对方法传入的参数及返回值进行约束
interface encrypt{
    (key:string, value:string):string
}
//名可以不同 但是类型得相同
let md5:encrypt = function(keys:string, values:string):string{
    return keys + values
}
console.log(md5('name', 'zhang'))

可索引接口

//对数组、对象的约束
var arr:number[] = [222,333] 正常定义数组的方式
var arr1:Array<string> = ['222','333']

interface UserArr{
    [index:number]:string   //number是索引值的数据类型 string是value的数据类型
}
let arr:UserArr = ['aaa', 'bbb']
console.log(arr[0])

interface UserObj{
    [index:string]:string | number
}
let obj:UserObj = {name:'张三', age:20}

类类型接口

//对类的约束 和 抽象类有点类似
interface Animal{
    name:string
    eat(str:string):void
}
class Dog implements Animal{
    name:string
    constructor(name:string){
        this.name = name
    }
    eat(){  //没有参数也可以
        console.log(this.name + '吃骨头')
    }
}
let d = new Dog('狗狗')
d.eat()
class Cat implements Animal{
    name:string
    constructor(name:string){
        this.name = name
    }
    eat(food:string){  //没有参数也可以
        console.log(this.name + '吃骨头')
    }
}
let c = new Cat('喵喵')
c.eat('food')

接口继承接口

interface Animal{
    getName():void
}
interface Person extends Animal{
   getFood():void
}
class Web implements Person{
    name:string
    food:string
    constructor(name:string, food:string){
        this.name = name
        this.food = food
    }
    getName(){
        console.log(this.name)
    }
    getFood(){
        console.log(this.name + '吃' + this.food)
    }
}
let w = new Web('小李', '米')
w.getFood()

继承结合接口

class Pro{
    work:string
    constructor(work:string){
        this.work = work
    }
}
class Code extends Pro implements Person{
    constructor(name:string, food:string, work:string){
        super(work)
    }
    getName(): void {
    }
    getFood(): void {
    }
}

接口和类型别名区别

两者都可以用来描述对象或函数的类型,但是语法不同。

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组。

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement('div');
type B = typeof div;

接口可以定义多次,类型别名不可以

interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };

TypeScript 泛型

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型约束

T extends object 是泛型约束的一种表现,泛型约束简单点说就是把泛型的具体化数据类型范围缩小。

interface Sizeable {
  size: number;
}
function trace<T extends Sizeable>(arg: T): T {
  console.log(arg.size);
  return arg;
}

泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

泛型函数

function getData<T>(value:T):T {
    return value
}
getData<number>(123)
getData<string>('123')
//或者可以将返回值设为any
function getNum<T>(value:T):any {
    return '1233'
}
getNum<number>(123)

泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者可以更好的学习其它的工具类型。详细可以查看必知必会TypeScript-自定义常用类型工具

TypeScript 装饰器

装饰器定义:一个方法或函数,可以注入到类 方法 属性参数上来扩展类 属性 方法 参数的功能。

装饰器写法:

  • 普通装饰器(无法传参)
  • 装饰器工厂函数(可传参) 装饰器的分类:
  • 类装饰器(Class decorators)
  • 方法装饰器(Property decorators)
  • 属性装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

ps: tsconfig.json 里面需要开启"experimentalDecorators": true

类装饰器

定义:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

类装饰器就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类 例子:
// 普通无参装饰器
function FirstClassDecorator(targetClass: any) {
  let targetClassObj = new targetClass()
  targetClassObj.buy()
  console.log('object :>> ', targetClassObj.name);
}
// 装饰器工厂
function FirstClassDecorator(params: string) {
  console.log('params :>> ', params);
  return function(targetClass: any) {
    let targetClassObj = new targetClass()
    targetClassObj.buy()
    console.log('object :>> ', targetClassObj.name);
  }
}
// 匿名工厂函数, 源码会加上class名称
function FirstClassDecorator(targetClass: any) {
  return class extends targetClass {
    constructor(...args: any) {
      super(...args)
    }
    methodone() {
      console.log('111 :>> ', 111);
    }
  }
}


@FirstClassDecorator('参数') // @FirstClassDecorator 不带参数
class CustomerService {
  name: string = '李四'
  constructor() {}
  buy() {
    console.log('object :>> ', this.name + '下单');
  }
}

方法装饰器

定义:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, 	 	
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

它接收三个参数:

  • target: Object - 被装饰的类的原型
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符
// 普通无参装饰器
function MymethodDecorator(targetClassPrototype: any, methodname: string, 
  methodsDecr: PropertyDescriptor) {
    console.log('object :>> ', targetClassPrototype, methodname, methodsDecr);
    methodsDecr.value()
}

// 匿名装饰器工厂函数
function MymethodDecorator(methodsPath: string) {
  console.log('methodsPath :>> ', methodsPath);
    return function (targetClassPrototype: any, methodname: string, 
      methodsDecr: PropertyDescriptor) {
        console.log('object :>> ', targetClassPrototype, methodname, methodsDecr);
        methodsDecr.value()
      }
}

class CustomerService {
  name: string = '李四'
  constructor(...args: any[]) {
    console.log('args :>> ', args);
  }
  @MymethodDecorator('/send')
  buy() {
    console.log('object :>> ', this.name + '下单');
  }
}

方法装饰器前置(后置)拦截器实现:

// 方法前置(后置)拦截器实现
function MymethodDecorator(methodsPath: string) {
    return function (targetClassPrototype: any, methodname: string, 
      methodsDecr: PropertyDescriptor) {
        let dataP = methodsDecr.value
        // console.log('object :>> ', targetClassPrototype, methodname, methodsDecr);
        methodsDecr.value()
        methodsDecr.value = function (...args: any) {
          args = args.map((arg: any) => {
            if (typeof arg === 'string') {
              return !!arg ? arg : ''
            }
            return arg
          })
          console.log('前者拦截 :>> ', args);
          dataP.call(this, args)
          console.log('后置拦截 :>> ', 122);
        }
      }
}
class CustomerService {
  name: string = '李四'
  constructor(...args: any[]) {
    console.log('args :>> ', args);
  }
  @MymethodDecorator('/send')
  buy() {
    console.log('object :>> ', this.name + '下单');
  }
}

属性装饰器

定义:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

它接收两个参数:

  • target: Object - 被装饰的类原型
  • propertyKey: string | symbol - 被装饰类的属性名
// 装饰器工厂函数
function Property(attrValue: any) {
  return function(targetClassPrototype: any, attrname: string | symbol) {
    console.log('targetClassPrototype :>> ', targetClassPrototype, attrname);
  }
} 
// 普通无参装饰器
function Property(targetClassPrototype: any, attrname: string | symbol) {
    console.log('targetClassPrototype :>> ', targetClassPrototype, attrname){
} 

class CustomerService {
  @Property('sss')
  name: string = '李四'
  constructor(...args: any[]) {
    console.log('args :>> ', args);
  }
  buy() {
    console.log('object :>> ', this.name + '下单');
  }
}

参数装饰器

定义:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

它接收三个参数:

  • target: Object - 被装饰的类原型
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值
// 普通装饰器
function UrlParam(targetClassPrototype: any, methodname: string, 
    paramindex: number) {
  targetClassPrototype.info = params
}
// 装饰器工厂函数
function UrlParam(params: any) {
  return function paramDecorator(targetClassPrototype: any, methodname: string, 
    paramindex: number) {
      targetClassPrototype.info = params
    }
}

class People {
  eat(@UrlParam('地址信息') address: string, who: string) {
    console.log('address :>> ', address);
  }
}

tsconfig.json

tsconfig.json 是 TypeScript 项目的配置文件。如果一个目录下存在一个 tsconfig.json 文件,那么往往意味着这个目录就是 TypeScript 项目的根目录。

tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

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          // 为装饰器提供元数据的支持
  }
}

参考文章