本文最后更新于 2025-03-03,文章内容可能已经过时。

https://ts.nodejs.cn/docs/handbook/typescript-from-scratch.htmlhttps://www.typescriptlang.org/play/?#code/PTAEHUFMBsGMHsC2lQBd5oBYoCoE8AHSAZVgCcBLA1UABWgEM8BzM+AVwDsATAGiwoBnUENANQAd0gAjQRVSQAUCEmYKsTKGYUAbpGF4OY0BoadYKdJMoL+gzAzIoz3UNEiPOofEVKVqAHSKymAAmkYI7NCuqGqcANag8ABmIjQUXrFOKBJMggBcISGgoAC0oACCbvCwDKgU8JkY7p7ehCTkVDQS2E6gnPCxGcwmZqDSTgzxxWWVoASMFmgYkAAeRJTInN3ymj4d-jSCeNsMq-wuoPaOltigAKoASgAywhK7SbGQZIIz5VWCFzSeCrZagNYbChbHaxUDcCjJZLfSDbExIAgUdxkUBIursJzCFJtXydajBBCcQQ0MwAUVWDEQC0gADVHBQGNJ3KAALygABEAAkYNAMOB4GRonzFBTBPB3AERcwABS0+mM9ysygc9wASmCKhwzQ8ZC8iHFzmB7BoXzcZmY7AYzEg-Fg0HUiQ58D0Ii8fLpDKZgj5SWxfPADlQAHJhAA5SASPlBFQAeS+ZHegmdWkgR1QjgUrmkeFATjNOmGWH0KAQiGhwkuNok4uiIgMHGxCyYrA4PCCJSAAhttps://www.bilibili.com/video/BV1YS411w7Bf/?spm_id_from=333.788.player.switch&vd_source=eecc2c8229facae905b6daed1650b44b&p=4

概述

TypeScript JavaScript 的一个超集,主要侧重于代码语法类型检查,在JS中,定义的所有类型都是静态类型,也就是弱类型语言,对与定义的变量无法正确的区分和使用,因此会出现不必要的问题产生

let funcTest = "func";
funcTest();
Uncaught TypeError: funcTest is not a function

当定义的变量不是一个function时,并当做一个function调用时,就会报一个TypeError类型错误!

12.toLowerCase();
Uncaught SyntaxError: Invalid or unexpected token

或者试着调用一个不存在的方法,就会在代码运行时,出现各种未知的问题!

从上面来看,当JavaScript运行代码之前,我们是很难确定语法类型的相关错误问题,这会导致我们只有去运行代码后,才会出现各种各样的未知问题!

基础部分

静态类型检查

静态类型检查,既是在编写代码的过程中,遇到类型,或者是错误方法一些不正常使用或调用时,会即刻编辑器中,通过红色波浪线去及时反馈开发者,此时将问题的描述会避免在代码运行时出现该问题,总结来说就是在代码运行之前做了静态类型语法逻辑校验!

非异常故障

非异常行为,包括,方法名拼写错误、调用对象中一个不存在的属性、函数调用时未加小括号等相关问题!

  1. 访问对象不存在的属性值,在js中会反馈一个undefind!

    const user = {
      name: "张三",
      age: 18
    }
    user.location
    
  2. 拼写错误检查

    const str = "Hello World";
    str.toLocaleLowerCased(); // toLocaleLowerCase();
    
  3. 检查逻辑语法问题

    const value = Math.random() < 0.5 ? '小于' : '大于';
    if( value !== '小于' ){
    
    } else if( value === '大于' ){
    
    }
    
    • ts

本地使用或安装

  1. 安装TypeScript!

    npm i typescript -g
    
  2. 在本地创建hello.ts文件,ts文件本身是无法运行的,需要通过TypeScript编译后,方可执行!

    const value = "hahahah";
    value.toLocaleUpperCase();
    console.log("Hello, world!" + value);
    
  3. 使用TypeScript进行文件编译

    tsc hello.ts
    
    • 编译后,会在本地生成一个.js的文件,可以使用node .js 来执行编译后的文件!
    node hello.js
    
  4. TypeScript基本命令:

    • 初始化ts配置文件tsconfig.js

      tsc --init
      
    • 自动编译ts文件

      tsc --watch
      
    • 类型检测有问题时,不去触发自动编译

      tsc --noEmitOnError hello.
      

修改入口目录和出口目录

我们可以通过 tsconfig.json中的配置[rootDir / outDir]来修改我们的指定目录:

rootDir: 是我们编写.ts文件代码的存放位置!

outDir: ts将 .ts 文件源代码编译后生成的 .js文件所存放的目录位置!

├── dist
│   └── hello.js
├── src
│   └── hello.ts
└── tsconfig.json

显示类型

显示类型,就是手动去标注参数变量的类型!

function greet( name: string, date:Date){
  console.log(`name: ${ name } today ${ date }`);
}

greet( "张三", new Date() );

typescript中去定义变量时,ts自动推断变量类型,当试图去修改变量值时,若新的值的类型不符合初始值类型时,则会报错!

let name = "张三";
name = "李四"; // ✔️
name = 12; // x

以上代码,将初始String类型修改为Number类型,这样是不允许的!

降级编译

ts默认编译环境为es2016模版字符串es6相关特性,如果在低版本浏览器中不兼容,还需对此进行适配操作!

tsconfig.json,找到 target 属性,其默认值为es2016,我门修改为es5进行降级编译,当通过ts编译时,就会适配es5!

{
  target: "es5"
}

常用类型

类型-string | number | boolean

js中,常用的类型有[string number boolean]!

let name: string = "张三";
let age: number = 18;
let flag: boolean = true;

以上代码,是在ts定义变量时的一种写法!

类型-any

any类型,从翻译表达来看,就是任意的意思,则表示任意类型,当某一个变量的类型为any类型时,那么从变量赋值,以及调用时,则不会触发相关的类型检查!

let obj: any = {
    name: 'zhangsan',
    age: 18
}

obj.foo();
obj.bar = "car";
obj();
const num:number = obj;

以上代码,ts类型检测正常,可以顺利编译成.js文件,但是当运行.js文件时,肯定会报错的!

注意: any 类型的变量,可以赋值其他任意类型变量!

let a: any = "hello";
let b: number = 99;
b = a;

类型-unknow

unknow 的含义就是未知的,一般用于定义个一个变量后期不知道要赋值与什么样的类型,它与any相比,更加安全,因为,unknow类型无法赋值给其它类型的值,而any是可以的!

let unknownVar: unknown;
let anyVar: any;
unknownVar = 10;
unknownVar = "hello";
unknownVar = true;

// 将未知类型赋值给 string 类型变量时,需要强制类型转换 as string
// let str1: string = unknownVar; // 错误
// let str1: string = anyVar; // 可以
let str2: string = unknownVar as string;

类型-never

never 的含义是从不,一般用于特殊函数返回值不推荐直接在变量上使用,因为在变量限制never类型会没有任何意义,且被限制never的变量无法进行赋值,否则会报错!

一般用于特殊函数返回值:

  1. 无限递归函数,不会正常执行结束的函数!

  2. 函数内部抛出异常时,且中断函数执行的函数!

function demo():never {
   demo();
}

function demo2():never {
   throw new Error("程序出现错误!")
}

类型-void

void 的含义是空虚的,一般用于函数返回值,当函数没有返回值时,可以使用void 来表示此函数的返回值!

function demo1( msg: string ):void {
  console.log("log msg", msg);
}

js 中,其函数内部没有返回值时,也会默认返回一个undefined!

ts 中,void 可以接受的类型其中就有undefined!

function demo1( msg:string ):void {
   console.log("log msg", msg);
   return undefined; 
}

function demo2( msg:string ):void {
   console.log("log msg", msg);
   return; 
}

以上代码,在void类型限制范围内!

function demo1( msg:string ):void {
   console.log("log msg", msg);
}
let result = demo1("你好啊!");
console.log("result", result) // 报错, void 是不允许其它变量来接受此返回值的,因为函数返回为空

function demo2( msg:string ):undefined {
   console.log("log msg", msg);
}
let result2 = demo2("你好啊2!")
console.log("result", result) // 不会报错, 此处undefined也是基本类型之一,在此作为undefined返回

void 无返回值的函数不允许其它变量接受此函数的执行结果,因为函数没有返回值!

类型-object(不推荐)

object含义就是对象,且该类型定义方向比较广泛数组 函数 以及 class 都可以作为object的类型值存储!

/* 
  1. object 类型表示非原始类型,可以存储任意类型的值
  2. 原始类型有 number string boolean null undefined symbol
  3. 原始类型不能作为 object 类型的值
*/
let obj1: object;

obj1 = {name: "Miao", age: 25};
obj1 = new String("Hello World");
obj1 = [1,2,3,4,5];
obj1 = function() {return "Hello World"};
obj1 = class Person{}

/* 无法存储原始类型 */
/* obj1 = 123;
obj1 = true;
obj1 = "Hello World";
obj1 = null;
obj1 = undefined; */

/* 
  1. Object 是所有类型的父类型
  2. 能够访问到Object对象中的方法,都可以作为存储使用
*/
let obj2: Object;

obj2 = {name: "Miao", age: 25};
obj2 = new String("Hello World");
obj2 = [1,2,3,4,5];
obj2 = function() {return "Hello World"};
obj2 = class Person{}

/* string number boolean 是因为有对应的包装类,所以可以存储 */
obj2 = 123;
obj2 = true;
obj2 = "Hello World";

由于object类型的限制范围比较广泛,所以这边不推荐object的直接使用!

类型-对象(推荐)

声明对象类型的方式

let person: { name: string, age: number };
// let person: { name: string; age: number };
/* let person: { 
     name: string
     age: number 
   }; */

person = { name: "Miao", age: 25 };

逗号分号,或者回车都可以作为每一个属性分隔符!

索引签名

索引签名,可以帮助我们有效的在一个对象类型动态添加属性属性值!

let person: { name: string, age: number, [key: string]: any };

person = { name: "Miao", age: 25, gender: "male", address: "China" };

/* 
  [key: string]: any 索引签名,可以存储任意属性
  gender: "male", address: "China" 动态添加属性值
*/

类型-数组

typescript 中定义数组类型有两种方式:

Type[] or Array[Type]

let arr1: number[] = [1,2,3];
let arr2: Array<number> = [1,2,3];
console.log(arr1, arr2);

以上代码,Type用来指定数组中的数据类型,限制为,该数组为一个存放Number类型的数组!

类型-函数

typescript中,我们可以为函数中的参数来指定type类型,同时也可以设置函数返回值类型!

function sayHello( name: string, age: number ): void{
  console.log(`hello ${name}, you are ${age}`);
}

sayHello('jack', 18);

void 表示该函数无返回值操作!

function sayHello( name: string, age: number ): number{
  console.log(`hello ${name}, you are ${age}`);
  return age;
}

sayHello('jack', 18);

number 表示返回一个Number类型!

如果参数没有指定类型的话,则ts则会自动推断参数值的类型!

function sayHello( name, age ): number{
  console.log(`hello ${name}, you are ${age}`);
  return age;
}

sayHello('jack', 18);

定义函数类型

/* 定义一个函数类型,且参数包括 a 和 b,返回值是 number */
let add: (a: number, b: number) => number;

/* add = function(a: number, b: number): number {
  return a + b;
} */
// 省略写法
add = function (a, b) {
  return a + b;
};

=> number 此代码,并非是箭头函数语法,且是ts中一种形式上的表示区分分隔符!

可选参数

可选参数,就是部分参数可传可不传,但是类型可能会为undefined!

  1. 函数中的可选参数

    function sayHello( name: string, age?: number ){
      console.log(`hello ${name}, you are ${age}`);
      return age;
    }
    
    sayHello('jack');
    
  2. 函数中的参数作为对象时的传参方式:

    function sayHello2( pt: { name: string, age: number } ){
      console.log(`hello ${pt.name}, you are ${pt.age}`);
      return pt.age;
    }
    
    sayHello2({ name: 'jack', age: 18 });
    
    • 如果 age 属性可传可不传时,可以在属性后方加入'?'即可!

      function sayHello2( pt: { name: string, age?: number } ){
        console.log(`hello ${pt.name}, you are ${pt?.age}`);
        return pt.age;
      }
      sayHello2({ name: 'jack' });
      
    • 当然,在取值age属性时,可能为undefined,所以通过对象的方式获取可选属性需要通过可选连的方式来获取

      pt?.age // 避免 age 属性为undefined时 报错问题
      

类型-元祖

元祖类型(tuple),是一个特殊的数组类型,将多种类型存放到一个数组当中!

let tuple: [string, number] = ["hello", 10];

console.log(tuple[0]); // "hello"
console.log(tuple[1]); // 10

/* ? 表示可选 */
let tuple1: [string, number?] = ["hello"];
console.log(tuple[0]); // "hello"

/* ...string[] 表示任意,后面无限多个 string 类型 可写可不写 类似于 any[] */
let tuple2: [number, ...string[]] = [1, "hello", "world"];
console.log(tuple2[0]); // 1
console.log(tuple2[1]); // "hello"
console.log(tuple2[2]); // "world"

类型-class

class PersonC {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  speck(){
    console.log("My name is " + this.name + " and I am " + this.age + " years old.");
  }
}

const p1 = new PersonC("Alice", 25);

class StudentC extends PersonC {
  grade: number;
  constructor(name: string, age: number, grade: number) {
    super(name, age);
    this.grade = grade;
  }
  study() {
    console.log("I am studying.");
  }
  override speck() {
    console.log("My name is " + this.name + " and I am " + this.age + " years old and I am in grade " + this.grade + ".");
  }
}

const s1 = new StudentC("Bob", 20, 3);
s1.speck();

属性修饰符

修饰符 含义 具体规则
public 公开的 类内部子类类外部访问
protected 受保护的 类内部子类访问
private 私有的 类内部访问
readyonly 只读的 属性无法修改
class PersonC {
  public name: string;
  public age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  speck() {
    console.log(
      "My name is " + this.name + " and I am " + this.age + " years old."
    );
  }
}

属性的简写形式

class PersonC {
  constructor(
    public name: string, 
    public age: number
  ) {}

  speck() {
    console.log(
      "My name is " + this.name + " and I am " + this.age + " years old."
    );
  }
}

高级部分

联合类型

联合类型,可以为一个变量指定多个类型的表示!

let flag: number | string | boolean = true;

flag = "1"

flag = 0;

flag = false;
function getLength(str: string | string[]){
  return str.length;
}

需要注意的是,当我们给一个变量,或者函数参数定义多种type类型时,我们在使用变量参数时,需要注意各自类型中是否存在相同的属性或方法,避免出现无法获取该属性方法的问题!

function getDetails( id: number | string ){
  // console.log(`id is ${id.toUperCase()}`);
  // 由于 id 可能是 number 类型,所以无法直接调用 toUperCase 方法
  if(typeof id === "string"){
    console.log(`id is ${id.toLowerCase()}`);
  }else{
    console.log(`id is ${id.toFixed()}`);
  }
}

以上代码,toLowerCase()方法是string中的方法,由于定义了number类型,所以传参时需要考虑会传递number类型的参数,此时若是number类型的值去调用toLowerCase时会出现问题!

type Gender = "男" | "女";
function printGender( gender: Gender ): void {
   console.log("性别: "+ gender)
}
printGender("男")

可以通过type定义联合类型,在函数传参时,可以得到友好的提示!

交叉类型

交叉类型,就是在原有的类型上进行扩展新的属性方法!

type StudentType = {
  name: string;
  age: number;
} & {
  gender: string;
};

类型别名

类型别名,就是将类型定义方式抽离到单独的定义方式,使用该类型时,直接通过别名引用即可!

type Person = {
  name: string;
  age: number;
};

function getPerson( p: Person){
  console.log(`name is ${p.name}, age is ${p.age}`);
}

getPerson({
  name: "zhangsan",
  age: 18
});

type Flag = number | string;

function getFlag(flag: Flag){
  console.log(flag);
}

也可以通过type定义函数类型,来限制函数参数类型,或者函数的返回值应该是如何表现!

type FunDemo = () => void;

const funDemo: FunDemo = function () {
  console.log("funDemo");
}

type FunDemo1 = (num: number) => void;

const funDemo1: FunDemo1 = function (num) {
  console.log("funDemo" + num);
};

funDemo1(1);

抽象类

抽象类,用于将属性方法单独抽象出一个类,则这个类不能被实例化继承抽象类的子类,必须实现抽象类中的所定义的方法 ,其内部抽象方法不能有具体的实现!

/* 定义一个包裹 */
abstract class Package{
  constructor( public weight: number ){}
  // 计算运费
  abstract calculateFee(): number;
  // 打印包裹信息
  printInfo(): void{
    console.log(`包裹重量:${this.weight}kg`);    
    console.log(`运费:${this.calculateFee()}元`);
  }
}

/* Package 是一个抽象类 不能被实例化 需要被继承后并实现其中的抽象方法*/
class Box extends Package {
  /* unitPrice 子类中的属性 需要加入修饰符 否则 this.unitPrice 无法访问 */
  constructor(weight: number, public unitPrice: number) {
    super(weight);
  }
  // 计算运费 重量乘以单价
  calculateFee(): number {
    return this.weight * this.unitPrice;
  }
}

const box = new Box(10, 5);
box.printInfo(); // 包裹重量:10kg 运费:20元

接口

接口类型别名用法大致相同,且接口能实现的,则类型也一样可以实现,但是类型一但被定义且无法再次进行修改添加字段!

接口内部主要是用于定义属性规范方法规范,且内部没有任何具体实现!

定义类的结构

实现接口通过implements来实现

/* 定义接口 */
interface PersonInterface {
  name: string;
  age: number;
  speck(n: number): void;
}

/* 实现接口 */
class PersonImpl implements PersonInterface {
  constructor(public name: string, public age: number) {}

  speck(n: number) {
    console.log(`${this.name} is ${n} years old`);
  }
}

const pi = new PersonImpl('Miao', 25);
pi.speck(25); // Miao is 25 years old

以上代码,接口定义了属性方法的规范,而实现这个接口,需按照规范实现!

定义对象解构

接口ts中,除了可以通过implements 来实现以外,也可以当做对象结构作为类型去限制!

interface UserInterface {
  name: string;
  age?: number;
  address: string;
  readonly idCard: string;
  run(): void;
}

const user: UserInterface = {
  name: "Miao",
  age: 25, // age 可选
  address: "Beijing",
  idCard: "1234567890",
  run() {
    console.log(`${this.name} is a runner`);
  },
};

user.run(); // Miao is a runner

定义函数接口

interface CountInterface {
  (a: number, b: number): number;
}

const count: CountInterface = (a, b) => a + b;

const count2: CountInterface = function (a, b) {
  return a + b;
}

接口之间继承

继承接口通过extends来实现

/* 
  接口 interface
*/

interface Animal {
  name: string;
  classify: string;
  eat(): void;
}
// 定义子接口并继承父接口的属性
interface Dog extends Animal {
  run(): void;
}
// 定义子接口并继承父接口的属性
interface Cat extends Animal {
  jump(): void;
}

function getDog(dog: Dog) {
  console.log(dog);
}

getDog({
  name: '旺财',
  classify: '犬类',
  eat() {
    console.log('旺财在吃狗粮');
  },
  run() {
    console.log('旺财在跑');
  }
})

function getCat(cat: Cat) {
  console.log(cat);
}

getCat({
  name: '招财猫',
  classify: '猫类',
  eat() {
    console.log('招财猫在吃猫粮');
  },
  jump() {
    console.log('招财猫在跳');
  }
})

以上代码,定义了一个父接口(Animal),两个子接口,分别将共用字段抽离成一个父接口,其它子接口分别去继承父接口的字段!

接口自动合并

定义重复的接口,在ts中这样是允许的,它最终会将两个接口不同定义的字段部分进行合并!

interface StudentInterface {
  name: string;
  age: number;
  grade: number;
}

interface StudentInterface {
  grade: number;
}

const s: StudentInterface = {
  name: "Miao",
  age: 25,
  grade: 3,
};

使用场景

定义接口使用场景:

  1. 定义对象的格式: 描述数据模型API响应数据格式配置对象等!

  2. 的约定: 规定一个,需要哪些属性方法!

  3. 自动合并: 一般用于第三方库的类型!

interfacetype 区别

相同点:两者都可以用于对象结构类型的定义与描述!

不同点:

  1. interface: 主要应用在对象之间的约定,可以实现类与类之间实现继承,以及自动合并!

  2. type:一般用于定义类型,不仅可以针对对象来约定类型,也可以针对简单类型进行约束,常用的有交叉类型、联合类型、类型别名!

  1. 扩展的方式不同:

    • 接口(interface)

      interface Dog extends Animal {
        run(): void;
      } 
      
    • 类型(type)

      type Dog = Animal & {
        run(): void;
      }
      
  2. 重复定义,并扩展新的字段:

    • 接口(interface)

      interface Articles {
        title: string
      }
      interface Articles {
        content: string
      }
      
      • 最终两个接口会合并
    • 类型(type)

      type Articles = {
        title: string
      }
      type Articles = {
        content: string
      }
      
      • 最终会报错
  3. 接口仅限制Object类型,而type可以定义基本类型,或复杂类型!

    • 接口(interface)

      interface Articles {
        content: string
      }
      
    • 类型(type)

      type name = string;
      type age = number;
      type friends = string[];
      type Articles = {
        title: string
      }
      

interfaceabstract 区别

相同点: 接口抽象类都能去定义类的格式!

不同点:

  1. 接口关注属性方法约定不关注其内部的实现,且实现多个接口,多个接口间使用逗号分隔!

  2. 抽象类 对于内部的抽象方法不会有具体的实现,但是普通方法可以定义实现逻辑的!

类型断言

TypeScript 中,类型断言(Type Assertion)是一种告诉编译器某个值的具体类型的方式。它允许你手动指定一个值的类型,从而绕过 TypeScript 的类型检查机制。类型断言不会改变变量运行时类型,只是在编译阶段提供类型信息。

当我们确定一个变量时,此时typescript无法推断出,此值的类型,那么这时候我们可以手动为该value值来指定一个类型!

其实有点像强制类型转换!

类型断言的语法

  1. as 语法(推荐):

    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;
    
  2. 尖括号语法:

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

手动为any类型来手动指定该值的确切类型!

let obj: any = { name: "Alice", age: 30 };
let nameLength = (obj as { name: string }).name.length;

这里手动配obj的确切类型为{ name: string }对象

自定义类型

自定义类型,就是通过文本的多个组合形式,来限制一个可选范围,包括参数,以及函数返回值等!

定义常量

类似于在js中声明一个const常量值,且只有一个值不能改变值!

let str: 'hello' = 'hello';

试着去修改 str

let str: 'hello' = 'hello';
str = 'hello2';

提示报错信息 不能将类型“"hello2"”分配给类型“"hello"”

限定类型可选范围

比如在函数中控制某一个参数类型的可选范围!

function setTextDirection(text: string, direction: 'left' | 'right' | 'center'){
  console.log(text +'is'+ direction);
}

setTextDirection("hello", "left") // or right / center
setTextDirection("hello", "center")

这里是设置文本的方向,我们将第二个方向direction参数可选类型范围规定为(left | right),当传参时,且只能是leftright!

定义函数返回值可选范围

function isExist( text: string, str: string) : true | false {
  return !(text.indexOf(str) === -1);
}

function isExist( text: string, str: string) : 1 | -1 {
  return text.indexOf(str) === -1 ? -1 : 1;
}

let index = isExist("hello is left", 'a');

console.log(index);

以上代码,主要是找出一段字符串中的某个字符,若找到,则返回 true 或 1 ,相反则为 false 或 -1!

请求案例

function request(url: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', data: any) {
  console.log(`Sending ${method} request to ${url} with data: ${data}`);
}

/* let options = {
  url: 'https://example.com',
  method: 'GET' as 'GET',
  data: { name: 'John' }
} */

let options = {
  url: 'https://example.com',
  method: 'GET',
  data: { name: 'John' }
} as const;
/* 
  // as 'GET' 限定 method 类型为 'GET',options.method 类型为 'string' 范围太大,无法确定 GET 还是 POST 所以不加会报错
  // as const 限定 options 类型为 { url: string, method: 'GET', data: any },options.method 类型为 'GET',options.data 类型为 any
*/
request(options.url, options.method, options.data); // Sending GET request to https://example.com with data: { name: 'John' } 'GET', { name: 'John' });

枚举

枚举ts中是通过 enum关键字实现的,主要用来定义一组固定的常量值,供业务模块去使用,当ts代码编译后,enum代码块就不存在了!

基本使用

enum Color {Red, Green, Blue};

let c: Color = Color.Green;
console.log(c); // output: 1

enum Direction {Up = 1, Down, Left, Right};

let d: Direction = Direction.Right;
console.log(d); // output: 3

使用场景

// 数字枚举
enum Status {
    Active,
    Inactive,
    Pending
}

console.log(Status.Active); // 输出: 0

// 字符串枚举
enum StatusString {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
    Pending = "PENDING"
}

console.log(StatusString.Active); // 输出: "ACTIVE"

// 常量枚举
const enum Direction {
    Up,
    Down,
    Left,
    Right
}

let directions = [Direction.Up, Direction.Down];
// 编译后等同于:
// let directions = [0, 1];

// 计算成员
enum BitFlags {
    None   = 0,
    Flag1  = 1 << 0,
    Flag2  = 1 << 1,
    Flag3  = 1 << 2,
    Flag4  = 1 << 3
}

console.log(BitFlags.Flag1); // 输出: 1
console.log(BitFlags.Flag2); // 输出: 2

常量枚举

常量枚举,是一种特殊的枚举类型,通过const来表示此枚举为常量枚举,其作用就是避免编译时生成过多的js代码!

// 常量枚举
const enum Direction {
    Up,
    Down,
    Left,
    Right
}

let directions = [Direction.Up, Direction.Down];

如果不使用const常量枚举时,编译时会生成过多的代码

var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 1] = "Up";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
    Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
;
let d = Direction.Right;
console.log(d); // output: 3

此时若加上const常量枚举时生成的代码

let d = 4 /* Direction.Right */;
console.log(d); // output: 3

泛型

泛型,当类型不确定时,我们可以通过泛型来动态设置类型的值!

泛型函数

/* 
  <T> 泛型参数 可以随便写,根据函数调用时,定义的类型为准
*/
function identity<T>(arg: T): T {
  console.log(arg);
  return arg;
}

identity<string>("hello");
identity<number>(1223);

泛型接口

/* 泛型接口 */
interface GenericIdentityFn<T> {
  name: string;
  age: number;
  extraInfo: T;
}

const identityFn: GenericIdentityFn<string> = {
  name: "miao",
  age: 20,
  extraInfo: "hello"
};

type obj = {
  addressInfo: string;
  email: number;
};

const identityType: GenericIdentityFn<obj> = {
  name: "miao",
  age: 20,
  extraInfo: {
    addressInfo: "beijing",
    email: 1234567890
  },
};

泛型类

/* 泛型类 */
class GenericNumber<T> {
  name: string;
  age: number;
  extraInfo: T;
  constructor(name: string, age: number, extraInfo: T) {
    this.name = name;
    this.age = age;
    this.extraInfo = extraInfo;
  }
  printExtraInfo() {
    console.log("this.extraInfo" + this.extraInfo);
  }
}
const gn = new GenericNumber<obj>("张三", 18, identityType.extraInfo);

类声明文件

类型声明文件作用就是支持在.ts文件中引入其它.js模块导出的属性和方法!

类型声明的文件名与js的文件名要保持一致!

  1. 新建 demo.d.ts 声明文件,以及demo.js模块文件:

    • demo.d.ts:

      declare function add(a: number, b: number): number;
      declare function subtract(a: number, b: number): number;
      export {greet, add, subtract};
      
    • demo.js:

      function add(a, b) {
        return a + b;
      }
      function subtract(a, b) {
        return a - b;
      }
      export { add, subtract };
      
  2. 新建.ts文件,并引入demo.js文件暴露的模块:

    import { add, subtract } from "./demo.js";
    
    add(1, 2);
    subtract(1, 2);
    greet("world", '2022');
    

装饰器

装饰器的根本是由函数实现的,目前装饰器功能还是实验性功能,需要开启配置后才能使用!

{
  "experimentalDecorators": true
}

修改 tsconfig.json 即可!

类装饰器

/* 
  类装饰器
  target 是 PersonDecorator 类
  Demo Fun 修改Person原型toString方法 返回JSON字符串
*/
function Demo(target: any) {
  target.prototype.toString = function(){
    return JSON.stringify(this);
  }
}

@Demo
class PersonDecorator {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const p = new PersonDecorator('miao', 20);
console.log(p.toString()); // {"name":"miao","age":20}

关于返回值

如果装饰器函数内部返回了一个新的class,那么会替换掉原有的class类!

function Demo(target: Function) {
  return class {
    name: string;
    age: number;
    constructor(name: string, age: number) {
      this.name = name;
      this.age = age;
    }
    test(){
      console.log('test');
      console.log("test");
      console.log("test");
    }
  };
}

@Demo
class PersonDecorator {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

关于构造类型

构造类型,就是通过new关键字可以实例化的函数,平时我们不能通过Function类型来限制,因为范围太大,且箭头函数也是函数,但是不能作为构造函数去实例化,所以需要排除Function类型的使用!

/* 
  1. new 表示:该类型可以通过new关键字去使用
  2. ...args 表示:该类型可以接收任意数量的实参
  3. any[] 表示:表示构造器可以接受任何类型的构造参数
  4. {} 表示:表示返回类型是一个对象(非null 和 undefined)对象
*/
type Constructor= new (...args: any[]) => {};

function Demo(target: Constructor) {
  return class {
    name: string;
    age: number;
    constructor(name: string, age: number) {
      this.name = name;
      this.age = age;
    }
    test() {
      console.log("test");
      console.log("test");
      console.log("test");
    }
  };
}

@Demo
class PersonDecorator {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const p = new PersonDecorator('miao', 20);
console.log(p.toString()); // {"name":"miao","age":20}

也可以约束静态属性

type Constructor= {
  new (...args: any[]): {},
  test: string; // 约束一个静态属性
};
@Demo
class PersonDecorator {
  name: string;
  age: number;
  static test: string = "test"; // 静态属性
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

替换被装饰的类

type Constructor1 = new (...args: any[]) => {};

function LogTime<T extends Constructor1>(target: T) {
  /* 继承 PersonDecorator1 类 */
  return class extends target {
    createTime: Date;
    constructor(...args: any[]) {
      console.time(target.name);
      super(...args);
      console.timeEnd(target.name);
      this.createTime = new Date();
    }
    getTime() {
      console.log(this.createTime);
    }
  };
}

@LogTime
class PersonDecorator1 {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
/* 添加类接口 进行约束,防止 以下两个新加属性无法被调用 */
interface PersonDecorator1 {
  createTime: Date;
  getTime(): void;
}

const pd1 = new PersonDecorator1('Miao', 25);
console.log(pd1.getTime());

装饰器工厂

装饰器工厂,允许我们在调用外部函数时传递参数,并且内部返回一个装饰器的函数!

type Constructor2 = new (...args: any[]) => {};

interface PersonFactory {
  introduce(): void;
}
function LogInfo(n: number) {
  return function(target: Constructor2) {
    target.prototype.introduce = function(){
      for(let i = 0; i < n; i++) {
        // console.log(`第${i + 1}次调用: ${this.speck() }`);
        this.speck();
      }
    };
  };
}

@LogInfo(5)
class PersonFactory {
  constructor(public name: string, public age: number) {}

  speck() {
    console.log(`你好啊 我是${this.name} 今年${this.age}岁了`);
  }
}


const pf = new PersonFactory("小明", 20);
pf.introduce(); // 第1次调用PersonFactory.speck()方法 第2次调用PersonFactory.speck()方法 第3次调用PersonFactory.speck()方法 第4次调用PersonFactory.speck()方法 第5次调用PersonFactory.speck()方法

装饰器组合

装饰器组合,就是多个装饰器在一个被装饰的或者方法使用多个装饰器,其顺序不同先从上往下执行装饰器工厂,其次在从下往上执行!

type ConstructorC = new (...args: any[]) => {};
function test1(target: ConstructorC) {
  console.log("test1");
}
function test2() {
  console.log("test2 工厂");
  return function (target: ConstructorC) {
    console.log("test2");
  };
}
function test3() {
  console.log("test3 工厂");
  return function (target: ConstructorC) {
    console.log("test3");
  };
}
function test4(target: ConstructorC) {
  console.log("test4");
}

@test1
@test2()
@test3()
@test4
class PersonCombination {}

执行结果

test2 工厂
test3 工厂
test4
test3
test2
test1

属性装饰器

属性进行拦截,可以监听属性值变化,在变化可以处理一些操作,比如依赖收集触发更新等相关操作!

/* 
  使用属性装饰器来监听属性值的变化
  静态属性 target 对应的是类
  实例属性 target 对应的是实例的原型对象
*/
function State(target: object, prototypeKey: string) {
  let key: string = `__${prototypeKey}`;
  console.log(typeof target, prototypeKey, key);
  Object.defineProperty(target, prototypeKey, {
    get() {
      return this[key];
    },
    set(newValue: any) {
      // 监听 prototypeKey 属性值的变化
      // console.log("watch newValue", newValue);
      // console.log("target", this, prototypeKey, key);

      this[key] = newValue;
    },
  });
}

class PersonAttr {
  name: string;
  @State age: number; // 对应的实力的 原型对象
  @State static address: string; // 对应的 类
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const pa = new PersonAttr("Miao", 25);
pa.age = 4;
console.log("pa: " + pa.age);
const pa1 = new PersonAttr("姜薇", 35);
pa1.age = 5;
console.log("pa1: " + pa1.age);

以上代码,用来监听属性的值的变化!

方法装饰器

可以对方法执行操作进行拦截,在执行前函数执行后打印输出内容!

/* 
  实例方法: target 为 类的原型对象
  静态方法: target 为 类本身
  prototypeKey: 当前方法的名字 【speck】
  descriptor: 当前方法的描述符

  Logger 在方法执行之前打印 日志 
         方法执行完毕后再次打印 日志  
*/
function Logger(
  target: object,
  prototypeKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(`target: ${target}`);
  console.log(`prototypeKey:  ${prototypeKey} `);
  console.log(`descriptor: ${descriptor}`);
  // 原方法 descriptor.value PersonFun类的原型对象上speck方法的描述符
  const originalMethod = descriptor.value;
  // 重写方法 当实例调用该方法是并传入参数时,通过call 或者 apply 调用原方法,并打印日志
  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${prototypeKey} 方法 之前执行....`);
    let result = originalMethod.apply(this, args);
    console.log(`调用 ${prototypeKey} 方法 之后执行....`);
    return result;
  };
  console.log("装饰器函数执行了!");
}

class PersonFun {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  @Logger
  speck() {
    console.log("我是" + this.name + ",今年" + this.age + "岁了!");
  }
  // 静态方法
  static sayHello() {}
}


const pf1 = new PersonFun("小明", 20);
pf1.speck(); // 调用 speck 方法 之前执行.... 调用 speck 方法 之后执行.... 我是小明,今年20岁了!

访问装饰器

访问装饰器,就是一个内部中的getset方法,当一个中有一个私有的方法或属性时,则其无法访问该私有属性方法,若想访问时,可以通过访问器的方式来提供外部使用!

/* 
  装饰器访问器
*/
function Range1( min: number, max: number ){
  return function (target: object, propertyKey: string, descriptor: PropertyDescriptor){
    // 重写 setter
    const originalSetter = descriptor.set;
    descriptor.set = function ( value: number){
      // 判断 value 是否在 min 和 max 之间
      if( value < min || value > max ){
        throw new Error(`Value must be between ${min} and ${max}`);
      }
      originalSetter?.call(this, value);
    };
  }
}
class Wetaher {
  private _temperature: number = 0;
  constructor(temperature: number) {
    this._temperature = temperature;
  }

  @Range1(0, 100)
  set temperature(value: number) {
    this._temperature = value;
  }
  get temperature() {
    return this._temperature;
  }
}

const w = new Wetaher(25);

// w.temperature = -10; // 访问器生效,设置属性值
w.temperature = 54;
console.log(w.temperature); // 访问器生效,获取属性值,输出 30

参数装饰器

参数装饰器,我们可以通过装饰器拦截参数,对参数进行一些逻辑操作,需要关注的是,参数对应方法实例方法时,则target对应的是类的原型对象,若是static静态方法时,则返回的是类的本身!

需要配合 方法装饰器来一起使用!

/* 
    装饰器参数
    target: 当前类的原型对象 如果是实例方法,则 target 为 类的原型对象,若是静态方法,则 target 为 类本身
    propertyKey: 当前属性的名字
    parameterIndex: 当前参数在列表中的索引
*/
function Params( target: any, propertyKey: string, parameterIndex: number ){
  console.log( `target: ${target}, propertyKey: ${propertyKey}, parameterIndex: ${parameterIndex}` );
}
class Classision {
  className: string;
  classNo: number;
  students: string[];
  course: string[] = ['数学', '语文', '英语'];
  constructor(className: string, classNo: number, students: string[], course?: string[]) {
    this.className = className;
    this.classNo = classNo;
    this.students = students;
  }

  addCourse( @Params course: string ) {
    this.course.push(course);
  }
}

const ci = new Classision('101', 1, ['小明', '小红']);
ci.addCourse('物理'); // target: [Function: Classision], propertyKey: addCourse, parameterIndex: 0
console.log(":ci", ci);