TypeScript基础

目录

TypeScript

TypeScript是什么

TypeScript是由微软基于JavaScript底层机制开发的扩展语言,并非全新的语言。
TypeScriptJavaScript的超集,它为JavaScript添加了类型系统,使得JavaScript代码更加健壮和可维护。
它只是为JavaScript加了静态类型检查、接口、泛型特性,并不会改变JavaScript语言机制。
由于JavaScript是动态类型,那么决定了TypeScript不可能是完全静态类型,更非强制静态类型,而java是强制静态类型。
强制静态化声明: 在编译时确定变量的类型,而不是在运行时确定变量的类型。

TypeScript的优势

  • 类型检查:TypeScript提供了静态类型检查,可以在编译时发现潜在的错误,提高代码的健壮性。
  • 代码提示:TypeScript提供了代码提示和自动补全功能,提高开发效率。
  • 面向对象编程:TypeScript支持面向对象编程的特性,如类、接口、继承等,使得代码更加模块化和可维护。

环境准备

  • 安装Node.js
  • 安装typescript编译器
➜  ts_demo git:(master) ✗ npm i -g typescript
added 1 package in 969ms

使用typescript编译ts文件

➜  ts_基础 git:(master) tsc hello.ts 

生成编译后的js文件
img

  • 安装VS Code开发工具
  • 安装VS插件TypeScript插件

自动化编译

1.创建tsconfig.json文件编译控制文件

➜  ts_基础 git:(master) ✗ tsc --init 

img

2.实时编译ts文件

➜  ts_基础 git:(master) ✗ tsc --watch

3.当ts代码出错时不再编译成js文件

使用命令方式

➜  ts_基础 git:(master) ✗ tsc --noEmitOnError --watch

或者使用tsc --init命令并配置tsconfig.json文件

{
  "compilerOptions": {
    "noEmitOnError": true
  }
}

类型声明

作用: 对变量或函数形参进行类型限制

基本类型声明

let str: string = 'hello';
let num: number = 123;
let bool: boolean = true;
let u: undefined = undefined;
let n: null = null;
let sym: symbol = Symbol();

数组类型声明

let arr: number[] = [1, 2, 3];

字面量声明

let str: 'hello'; // 后续只能将str变量赋值为 hello 字符串

str = 'hello';

字面量定义变量类型,并没有赋值,不能直接使用
img

函数参数类型声明

js未对函数入参类型限制,可能得到预期不一样的结果

function add(firstNum, secondeNum) {
  return firstNum + secondeNum;
}
console.log(add(1, '2')); // js输出 字符串12

ts对函数参数类型声明,可以避免上述问题
img


声明变量fn为函数,并限制形参数量和类型,和返回值类型

let fn: () => string;
fn = function () {
  return 'hello world';
};

类型推断

在没有对变量声明类型时就赋值,ts会进行推断该变量的类型,后续只能使用该类型数据进行从新赋值

let num  = 1;
num = false; // 报错 Type 'false' is not assignable to type 'number'.

类型总览

javascript中的数据类型

1.string
2.number
3.boolean
4.undefined
5.null
6.bigint
7.symbol
8.object

其中object包含: Array、Function、Date、RegExp、Error、Math、JSON、Promise等

typescript中的数据类型

1.上述js中所有类型
2.6个新类型: voidanyneverunknowntupleenum
3.两个用于自定义类型的方式: interfacetype

小写类型和大写类型区别

小写类型: 通过原始定义变量,如numberstringbooleanundefinednullsymbol
大写类型: 通过包装对象定义变变量,如NumberStringBooleanUndefinedNullSymbol

ts中变量的定义区分包装类型定义和原始类型定义,不能将包装类型赋值给原始类型定义变量,可以将原始类型变量赋给包装类型变量
img

对于object类型,则不区分原始类型定义和包装类型定义,它们都是object类型,因此可以相互赋值

let obj: object = {};
let obj2: Object = new Object();
obj = new Object();

基本数据类型numberstringboolean一般都是小写

let a: number = 1; // number类型
let b: string = 'hello'; // string类型
let c: boolean = true; // boolean类型

java中允许将包装类型赋值给原始类型变量

int c = 1;
c = new Integer(2);
System.out.println(c); // 输出: 2

小写定义类型属于基本类型,object除外,大写定义类型是一个包装类型,包装类型属于object类型

let str: string = 'hello world';
let str2: String = new String('123');
console.log(typeof str, typeof str2); // 输出: string object

注意

JavaScript中的这些内置构造函数:NumberStringBoolean,⽤于
创建对应的包装对象, 在⽇常开发时很少使⽤,在TypeScript中也是同理,所以
TypeScript中进⾏类型声明时,通常都是⽤⼩写的numberstringboolean


常用类型与语法

any任意类型

any类型表示任意类型,一旦将变量限制为any类型,意味着不再对该变量进行类型检查,可以随时赋值不同类型数据改变变量的类型

let a: any;
a = 1;
console.log(typeof a); // number
a = "hello";
console.log(typeof a); // string

显示any: let a: any;
隐式any: let a;


可以把any类型的变量赋给其他类型变量,那么其他变量的类型也会变为any类型

let a: any;
console.log(typeof a); // undefined
a = 1;
console.log(typeof a); // number
let str: string;
str = a;
console.log(typeof str); // number
str = '123';
console.log(typeof str); // string

unknown未知类型

unknown类型表示未知类型,可以理解为更为安全的any类型

any: 可以将any类型变量赋值给任意类型的目标变量,目标变量也会变成any类型
unknown: 和any一样赋值后确定变量的类型,但是unknown不能直接赋值给其他变量

img

unknown类型赋值给对应类型变量的方式

1.通过判断unknown类型变量的具体类型后,将其赋值给其他相同类型变量

let a: unknown;
a = "hello";
let b: String;
if (typeof a === "string") {
  b = a;
}

2.通过类型断言将unknown类型变量赋值给其他类型变量

在unknown变量后 as 类型,标记为确定类型后,将其赋值给相同类型的变量
或者在unknown变量前使用<类型>,也可以确定unknown变量的具体类型

let a: unknown;
a = "hello";
let b: String;
b = a as String;
// 或者 b = <String>a;

img
通过断言标记unknown变量为指定类型

let a: unknown;
a = 'hello';
(a as string).toUpperCase();

never永不存在的类型

never类型含义: 永远不存在具体值的类型,什么值都不行,null''undefined0false等都是具体值,所以不能赋值给never类型变量,
never类型往往都是ts自己推断出的类型

使用场景

1.几乎不用never去限制变量,没有任何意义

let a: never;
a = 1; // 报错 Type '1' is not assignable to type 'never'.

2.ts推断函数返回值为never类型,或变量为never类型

return后面表达式抛出异常,ts会自动判断fail的函数为never类型

function fail(){
  return error("失败");
}

由于永远不会执行else逻辑,因此else中变量为never类型
img


3.never用于限制函数的返回值: 限制函数不能有返回值,使用never限制的函数,该函数要么是抛出异常的函数,要么就是永远不会结束的函数

// 抛出了异常,该函数没有任何返回值
function error(msg: string): never {
  throw new Error(msg);
}
// 函数内部是一个死循环,该函数没有任何返回值
function loop(): never {
  while (true) {}
}

void空类型,只允许返回undefined

使用场景

1.void类型通常用于函数返回值的声明,含义: 开发者定义的函数没有返回值,调用者不需要处理返回值。默认情况下函数会返回undefined,因此声明了void函数可以返回undefined

function say(): void{
  console.log("hello world");
}

注意: void声明的函数允许返回undefined空类型

理解void和undefined

undefined: 未定义类型,函数默认返回都为undefined,因此声明了void函数可以返回undefined
void: void包含undefined,意义在于函数返回值语义上的约定,不允许调用者使用其返回值进行逻辑运算
返回void不允许将结果参与运算,返回undefined可以参与运算,因此void和undefined是不同的
img


void失效场景

直接将void定义在函数体前是生效的,不能有返回值
img

如下代码,声明fn为函数,并限制返回类型为void,但在对fn进行赋值为函数时,函数仍能定义返回值,导致void失效

let fn: () => void;
fn = function () {
  return "hello world";
};
console.log(fn()); // 输出: hello world

原因: ts要保证js内置对象的回调函数能够正常返回值

// 使用map对数组进行加工
let arr = [1, 2];
let add: (a: number) => void;
add = function (item) {
  return item + 1;
};
let newArr = arr.map(add);
console.log(newArr); // 输出: [2, 3]

如上代码,假如定义的add回调函数不允许有返回值那么数组的map方法将会失效,因此为了避免这种情况,先声明的void类型将失效,而是应该在定义函数时函数体前添加void
img


object对象类型

小写object和大写Object区别

关于object与Object结论: 实际开发中使用很少,因为几乎没有限制变量类型

1.object类型表示非原始类型,即非numberstringbooleansymbolbigintnullundefined类型
例如可以是: {}, [], function(){}, new Object()
img
2.Object类型表示所有对象类型的父类型,即所有对象类型都是Object的子类型,它的限制更加宽泛,几乎允许存放所有类型数据
但是null和undefined除外
img


声明对象类型

let person: { name: string; age: number };
let person1: { name: string; age: number };
let person2: {
  name: string;
  age: number;
};
person = { name: "张三", age: 18 };
person1 = { name: "张三", age: 18 };
person2 = { name: "张三", age: 18 };

1.通过字面量声明变量类型为对象,并指定对象属性有哪些
2.属性之间可以通过逗号,分号,换行进行分隔
3.对象属性后添加?表示该属性可选

let person: { name: string; age?: number };
person = { name: "张三" }; // 可以不指定age属性

如果不指定可选属性,那么在赋值对象变量时,所有的属性必须赋值
img
4.通过索引签名定义对象属性,允许对象具有任意数量的属性,这些属性的键和类型是可变的,常用于不确定对象中有哪些属性时场景
可以添加任意属性,但对已声明的属性类型仍有限制

let person: { name: string; age?: string; [key: string]: any };
person = { name: "张三", gender: "男" }; // 可以添加任意属性,但对已声明的属性类型仍有限制  

ts为什么允许对象动态添加any属性

原因: TypeScript核心使命是为JavaScript提供类型工具,不可能改变JavaScript动态类型语言的机制,因此TypeScript允许对象动态添加any属性


声明函数类型

TS中声明函数: 函数名: (形参: 类型..) => 返回值类型
也可以直接定义函数时约束函数参数和返回值类型: 函数名(形参: 类型..): 返回值类型 {函数体}

let add: (a: number, b: number) => number;
add = (a: number, b: number): number => {
  return a + b;
};
const add2 = function (a: number, b: number): number {
  return a + b;
};
function add3(a: number, b: number): number {
  return a + b;
}

TypeScript中的=>表示函数参数和返回值类型,而不是箭头函数


声明数组类型

let arr: number[];
arr = [1, 2, 3];
let arr1: Array<number>;
arr1 = [1, 2, 3];

tuple元组

tuple(/tjuːpəl/)元组: 是一种特殊的数组类型,固定长度的数组,每个元素都有具体类型,且类型可以不同

 let arr: [string, number?, ...string[]];
 arr = ['hello', 123];
 arr = ['hello'];
 arr = ['hello',1, '123','1231`'];

可选元素一定要位于必选元素的后面

img

可选元素如果位于扩展运算元素前面,那么将成为必选元素

img

可选元素不能放在扩展运算元素的后面

img


enum枚举

枚举: 定义一组常量

数字枚举

数字枚举: 其成员的值默认从0自增,具有反向映射特点

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
console.log(Direction);
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
console.log(Direction);

枚举类型数据将自动从0开始递增,以及反向索引
img

设置枚举的初始值

enum Direction {
  UP = 3,
  DOWN,
  LEFT = 6,
  RIGHT,
}
console.log(Direction);

img


字符串枚举

字符串枚举: 其成员的值都是字符串类型,没有反向映射特性

enum Direction {
  UP = "UP",
  DOWN = "DOWN",
  LEFT = "LEFT",
  RIGHT = "RIGHT",
}
console.log(Direction);

img


常量枚举

常量枚举: 使用const关键字定义枚举,常量枚举在编译阶段会被删除,不会生成任何js代码,常量枚举成员的值在编译阶段就会被确定下来

非常量枚举:

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
console.log(Direction.UP);

非常量枚举编译为js后,产生的js代码如下:

"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["UP"] = 0] = "UP";
    Direction[Direction["DOWN"] = 1] = "DOWN";
    Direction[Direction["LEFT"] = 2] = "LEFT";
    Direction[Direction["RIGHT"] = 3] = "RIGHT";
})(Direction || (Direction = {}));
console.log(Direction.UP);

常量枚举:

const enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
console.log(Direction.UP);

编译为js后代码,在编译阶段就已经将Direction.UP转为0,js不会产生其他代码:

"use strict";
console.log(0 /* Direction.UP */);

type自定义类型

type关键字用于定义自定义类型,可以定义联合类型、交叉类型、字面量类型、类型别名等

类型别名

type num = number;
let a: num;
a = 123;
a = '123'; // 报错

当自定义函数参数类型时,可以指定函数形参类型以及返参类型,而且该类型可以用在表达式函数的函数名后,例如: 表达式函数名: 自定义函数类型 = function(){}

自定义一个函数类型,并且该函数不接受任意参数,返回值为number

type fn = () => number;
let add:fn = function(){
  return 123;
}

联合类型

使变量允许存储多种类型

let str: string | number;
str = "hello";
str = 123;

type Status = number | string;
function getStatus(status: Status) {
  return status;
}
getStatus(404);
getStatus("404");

字面量类型

字面量类型: 允许将字面量类型作为类型注解使用,字面量类型只能用于变量、属性、函数参数、函数返回值等

let str: "hello";
str = "hello";
str = "world"; // 报错

type Status = "success" | "error";
function getStatus(status: Status) {
  return status;
}
getStatus("success");
getStatus("error");
getStatus("warning"); // 报错

交叉类型

交叉类型: 将多个类型合并为一个类型,使用&符号连接多个类型,每一个类型用于限定创建的对象属性以及属性的类型,因此交叉类型可以用于对对象进行类型合并

使用&交叉两个type

type User = {
  name: string;
  age: number;
};
type Admin = {
  role: string;
};
type Person = User & Admin;
let person: Person = {
  name: "张三",
  age: 18,
  role: "admin",
};

使用&交叉两个对象作为一个type

type Person = {
  name: string;
  age: number;
} & {
  sayHello(): string;
};
let kevin: Person = {
  name: 'kevin',
  age: 18,
  sayHello() {
    return `hello, my name is ${this.name}`;
  },
};

类的声明

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`hello,my name is ${this.name},i am ${this.age} years old`);
  }
  static getInfo() {
    console.log("hello,i am a person");
  }
}
class Student extends Person {
  constructor(name: string, age: number) {
    super(name, age);
  }
  override sayHello() {
    console.log(
      `hello,my name is ${this.name},i am ${this.age} years old,i am studying`
    );
  }
}
let s = new Student("张三", 18);
console.log(s); // 输出: Student { name: '张三', age: 18 }
s.sayHello(); // 输出: hello,my name is 张三,i am 18 years old,i am studying
Student.getInfo(); // 输出: hello,i am a person

1.使用override关键字来重写父类的方法
2.继承可以继承父类的属性、方法、静态方法、静态属性


类的属性简写

constructor形参前必须添加修饰符

class Person {
  constructor(public name: string, public age: number) {}
  sayHello() {
    console.log(`hello,my name is ${this.name},i am ${this.age} years old`);
  }
}

简写形式必须添加修饰符,如果继承了父类,那么可以省略修饰符
img

由于父类已经添加了变量修饰符,子类可以省略修饰符

class Person {
  constructor(public name: string, public age: number) {}
  sayHello() {
    console.log(`hello,my name is ${this.name},i am ${this.age} years old`);
  }
}
class Student extends Person{
  constructor(name: string,age:number){
    super(name,age);
  }
}

类的修饰符

属性和方法的默认修饰符为public

类中资源允许被访问的范围: 类内部、子类、类外部

修饰符权限:

修饰符 含义 类内部 子类 类外部
public 公开
protected 受保护 x
private 私有 x x
readonly 只读 不可修改 不可修改 不可修改

1.public: 公有属性,可以在类的内部、子类、类的外部访问

class Person {
  constructor(public name: string, public age: number) {}
  sayHello() {
    // 类内部可以访问public权限资源
    console.log(`hello,my name is ${this.name},i am ${this.age} years old`);
  }
  static getInfo() {
    console.log("hello,i am a person");
  }
}
class Student extends Person {
  constructor(public name: string, public age: number) {
    super(name, age);
  }
  override sayHello() {
    console.log(
      // 子类可以访问父类中public权限资源
      `hello,my name is ${this.name},i am ${this.age} years old,i am studying`
    );
  }
}
let p = new Person("张三", 18);
p.name; // 类外部可以访问类中public权限资源

2.protected: 受保护属性,可以在类的内部、子类访问,类的外部无法访问

类内部和子类可以访问,类外部无法访问
img

3.private: 私有属性,可以在类的内部访问,子类、类外无法访问

类内部可以访问,子类和类外部无法访问
img

通过提供public的get方法访问私有成员变量

class Person {
  constructor(private name: string, public age: number) {}
  get getName(): string {
    return this.name;
  }
}
let p = new Person("张三", 18);
console.log(p.getName); // 通过public的get方法访问私有成员变量 输出: 张三

4.readonly: 只读属性,在任何地方都不能修改,只能读取

img


抽象类

抽象类: 不能被实例化的类,不能通过new关键字创建实例,只能被继承,可以有抽象方法,也可以有具体实现方法
抽象类中可以定义变量、构造方法、抽象方法、普通方法
构造方法作用: 虽然抽象类不能被实例化,但是允许有构造方法,也就意味着抽象类可以被继承,用于子类在实例化时调用父类的构造方法,初始化父类的属性,
也就是说抽象类中构造方法用于通过子类来初始化其属性,子类必须通过super()方法调用父类的构造方法,初始化父类的属性

abstract class Express {
  // 构造中定义公共属性
  constructor(public name: string, public address: string) {}
  // 定义抽象方法,子类实现不同逻辑
  abstract send(): void;
  // 定义公共普通方法
  printPackageInfo() {
    console.log(`快递:${this.name},目的地:${this.address}`);
    this.send();
  }
}
class ems extends Express {
  constructor(name: string, address: string) {
    super(name, address);
  }
  override send() {
    console.log("您的包裹由EMS快递公司负责配送");
  }
}
class sf extends Express {
  constructor(name: string, address: string) {
    super(name, address);
  }
  override send() {
    console.log("您的包裹由顺丰快递公司负责配送");
  }
}
const emsExpress = new ems("ems", "北京");
emsExpress.printPackageInfo();
const sfExpress = new sf("sf", "上海");
sfExpress.printPackageInfo();

子类重写抽象方法实现自己的逻辑
img


抽象类使用场景

1.为同类逻辑抽离出一个公共抽象方法
2.为同类属性抽离出公共属性放到抽象类中
3.为子类提供通用的公共方法实现
4.确保子类自身必须实现某个功能逻辑(抽象方法必须被重写)
img
5.用于封装公共属性和方法实现,子类继承抽象类后,可以复用抽象类中的公共属性和方法


interface接口

接口是定义结构的方式,主要为类、对象、函数规定一种契约。
接口中可以声明属性、方法,但没有构造方法和普通方法
由于没有构造方法意味着接口不能被子类继承,只能被子类实现,接口允许被其他接口继承

接口中不能初始化成员变量,只能声明成员变量,且只能允许readonly修饰

img

为类定义结构

interface IPerson {
  name: string;
  age: number;
  say(): void;
}

class Person implements IPerson {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  say() {
    console.log(`${this.name} is ${this.age} years old.`);
  }
}

为对象定义结构

interface当做类型作用在对象上,限定对象成员属性、方法。

约束对象中必须有name、age属性和say方法实现,sex属性可选

interface IPerson {
  name: string;
  age: number;
  sex?: string;
  say(): void;
}

let Person: IPerson = {
  name: "1",
  age: 18,
  say() {
    console.log(`${this.name} is ${this.age} years old.`);
  },
};

为函数定义结构

interface IPerson {
  (name: string, age: number): number;
}
const fn: IPerson = function (name: string, age: number) {
  return age;
};

接口之间的继承

img


接口可重复定义,且同时生效(声明合并)

有两个相同名称的IPerson接口,kevin对象收到了IPerson接口的约束,那么两个IPerson接口的约束生效
img


接口和类同名时合并特性

interface Person {
  name:string;
}
class Person {}
let a = new Person();
a.name; // a.name 不报错,编译通过

当一个接口和一个同名的类被声明时,TypeScript 会自动将它们合并。类的实例会被推断为符合接口的形状。因此,new Person() 创建的实例会被推断为符合 Person 接口的对象
这种合并并不意味着类中实际存在该属性,除非你在类的构造函数中显式定义它。如果你尝试访问 name,而没有为 Person 类添加该属性,运行时会抛出错误(因为 name 不会自动添加到类的实例中)。但在编译阶段,TypeScript 认为这个属性存在。


总结

使用场景
1.通常使用接口定义数据模型,API入参和返参数据模型,配置对象等等,是开发中最常用定义数据结构的方式
2.对子类或抽象类的约束: 规定一个类必须实现哪些属性和方法,规定抽象类中必须实现哪些方法
3.自动合并: 一般用于扩展第三方库的类型,这种特性在大型项目中可能用到


interface和type的区别

相同点:
interface和type都可以定义对象数据结构
不同点:
interface更加专注定义对象的数据结构,支持继承合并
type更加专注定义类型别名联合类型交叉类型,但不支持继承和自动合并


interface和abstract抽象类区别

interface: 只能描述成员结构,不能有任何实现方法,一个类可以实现多个接口,接口之间可以继承,也可以继承抽象类
abstract class: 即可以描述成员结构,也可以有实现方法,一个类只能继承一个抽象类,抽象类之间可以继承,也可以实现接口


接口可以继承抽象类和普通类: 继承后该接口也就具有了抽象类或普通类的成员定义和方法定义

接口继承抽象类:
img
接口继承普通类:

class World {
  name: string = 'World';
  say() {}
}
interface MyInterface extends World {
}
class Hello implements MyInterface {
  name: string = 'Hello';
  say() {}
}

抽象类之间可以继承: 子抽象类继承了父抽象类的成员

abstract class Hello {
  constructor(public name: string) {}
  abstract sayWorld(): void;
  say(): void {}
}
abstract class World extends Hello {}
class HelloWorld extends World {
  sayHello(): void {
    console.log("Hello");
  }
  sayWorld(): void {
    console.log("World");
  }
  say() {
    console.log("Hello World");
  }
}

抽象类可以实现接口: 约束了抽象类中必须有抽象或实现接口中的方法,抽象类必须声明接口中的属性

interface World {
  name: string;
  sayWorld(): void;
  say(): void;
}
abstract class Hello implements World {
  constructor(public name: string) {}
  abstract sayWorld(): void;
  say(): void {}
}

ts和java相比:
1.java中接口是不能继承抽象类或普通类的,而ts既可以继承抽象类也可以继承普通类
2.java中抽象类是可以实现接口的,而ts也可以


泛型

泛型: 允许我们在定义函数、类、接口时,使用类型参数表示未指定的类型,这些参数在具体使用时才会指定具体的类型。
泛型能让同一段代码适用于多种类型,从而提高代码的复用性。泛型就是解决类、接口、方法重复使用的一种技术。

泛型函数

调用者传入参数的类型,函数内部使用的类型。

function say<T>(message: T) {
  console.log(message);
}
say<string>("hello ts"); // 输出: hello ts
say<number>(123); // 输出: 123

泛型可以有多个

function say<T, R>(message: T, info: R) {
  console.log(message, info);
}
say<string, number>("hello ts", 123); // 输出: hello ts 123

泛型接口

interface IPerson<T> {
  name:string;
  extraInfo: T;
}
interface IStudent{
  age: number;
  hobby: string[]
}
const kevin: IPerson<IStudent> = {
  name: 'kevin',
  extraInfo: {
    age: 18,
    hobby: ['basketball', 'football']
  }
}

泛型类

class Person<T> {
  constructor(public name: T) {}
  getName(): T {
    return this.name;
  }
}
const p = new Person<string>("张三");

泛型约束

限定T必须具有ILength接口中的属性的对象,T包含了length属性,在泛型函数中才能使用length属性

interface ILength {
  length: number;
}
// 限定T必须是实现了ILength接口的类,T包含了length属性
function say<T extends ILength>(message: T) {
  console.log(message.length);
}
say<string>("hello ts"); // 输出: 8
say<number[]>([1, 2, 3]); // 输出: 3
say({ length: 10 }); // 输出: 10

类型声明文件

类型声明文件: *.d.ts文件,用来为js文件提供类型声明,让ts能够识别js文件中的类型。
类型声明文件中,一般不会出现具体的实现代码,只会出现类型声明。
作用: 提供js代码的类型信息,使得ts能够引入js第三方库或模块化时进行类型检查和提示

demo.js

export function add(a, b) {
  return a + b;
}
export function sub(a, b) {
  return a - b;
}

demo.d.ts: 为demo.js提供类型声明

declare function add(a: number, b: number): number;
declare function sub(a: number, b: number): number;
export { add, sub };

index.ts使用js资源

import  {add, sub} from "./demo.js";
console.log(add(1, 2)); // 输出: 3

index.html: 引入ts编译好的index.js文件

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script type="module" src="index.js"></script>
</head>
<body>
</body>
</html>

注释掉tsconfig.json"module": "commonjs"配置


装饰器

装饰器: 本质是一种特殊的函数,可以修改类的行为,装饰器可以用来装饰类、方法、属性、参数等。
装饰器种类:
1.类装饰器
2.方法装饰器
3.属性装饰器
4.参数装饰器
5.访问器装饰器

类装饰器

类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能逻辑

装饰器执行时机: 当被装饰类加载完成时,再执行装饰器函数

基本语法

1.定义装饰器函数Demo,参数target表示被装饰的类
2.在Demo函数内部编写逻辑代码,对target进行操作
3.在需要装饰的类上方使用注解@装饰器函数名称进行装饰

function Demo(target: Function) {
  console.log(target);
}
@Demo
class Person {
  constructor(public name: string, public age: number) {
  }
  say() {
    console.log(`我叫${this.name},今年${this.age}岁`);
  }
  
}
const p = new Person('张三', 18);
p.say();

img


应用举例

需求: 定义一个装饰器,实现Person实例调用toString方法时,返回实例的json字符串

function ToJson(target: Function) {
  target.prototype.toString = function () {
    return JSON.stringify(this);
  }
}
@ToJson
class Person {
  constructor(public name: string, public age: number) {
  }
  say() {
    console.log(`我叫${this.name},今年${this.age}岁`);
  }
}
const p = new Person('张三', 18);
console.log(p.toString()); // 输出: {"name":"张三","age":18}

使用Object.seal(target.prototype);禁止对类的原型添加属性

function RefuseNewAttr(target: Function) {
  Object.seal(target.prototype);
}
@RefuseNewAttr
class Person {
  constructor(public name: string, public age: number) {
  }
  say() {
    console.log(`我叫${this.name},今年${this.age}岁`);
  }
}
// @ts-ignore
Person.prototype.hhh = 'hhh'

img


装饰器返回值

装饰器函数可以返回一个函数,返回的函数会替换掉被装饰的类,实例对象会指向返回的类

function Demo(target: Function) {
  return class {
    test() { 
      console.log("demo");
    }
  };
}
@Demo
class Person {
  test() {
    console.log("test");
  }
}
console.log(Person);
new Person().test();

装饰器返回了一个类,那么Person类就会被替换成返回的类,实例对象会指向返回的类
img


关于构造类型

在TypeScript中,Function类型所表示的范围十分广泛,可以是普通函数,箭头函数,方法,构造函数等等。但并非Function类型的函数都可以被new关键字实例化,例如箭头函数就不能实例化,因此需要限制类型该类型可以通过new构造函数进行实例化

仅构造类型的约束

/**
 * new: 该类型可以通过new关键字来创建实例
 * ()=>{}: 表示是一个函数类型  
 * (...args:any[]): 表示该函数可以接收任意数量和任意类型的参数  
 * {}: 表示该函数返回值为对象类型(非null,非undefined的对象)
 */
type Constructor = new (...args: any[]) => {};
function demo(fn: Constructor){};
class Person{}; // 类允许通过new关键字创建实例
demo(Person);
let obj = {
  fn(){}
}
demo(obj.fn); // 对象中的方法不能通过new创建实例
function Fn(){}
demo(Fn); // ts中不能通过new关键字创建函数的实例,而js中是允许的
let fn2 = ()=>{};
demo(fn2); // 箭头函数不能通过new关键字创建实例

img

或者使用泛型约束对函数参数进行约束

type Constructor = new (...args: any[]) => {};
function demo<T extends Constructor>(fn: T) {} // 等同于function demo(fn: Constructor) {}

(target: Constructor)<T extends Constructor>(target: T)的区别

无泛型 (Constructor):target 只是被认为是一个构造函数类型,且没有更具体的类型推断。这种方式对所有构造函数都适用,不管是哪个具体类型的构造函数。
带泛型 (T extends Constructor):target 的类型会被推断为传递给 Demo 的具体构造函数类型,提供更强的类型推导支持,允许你在装饰器内更准确地使用 target。

type Constructor = new (...args: any[]) => {};
function Demo(target: Constructor) {
  // target 是一个构造函数,不能推断出更具体的类型
}
type Constructor = new (...args: any[]) => {};
function Demo<T extends Constructor>(target: T) {
  // target 的类型会被推断为 T,即传递给 Demo 的具体构造函数类型,可以更准确地使用 target
}

当函数入参只是构造函数类型,该函数返回继承了入参函数时,那么返回的函数必须包含入参函数的所有属性,否则会报错
img
而使用泛型,则会自动推断出入参为具体的类型,因此在使用extends时可以完全继承入参的所有属性
img


js中允许通过new创建函数实例,而ts中不允许通过new创建函数实例

function Person(name) {
    this.name = name;
}
let p = new Person('张三');
console.log(p.name); // 输出: 张三

构造类型 + 静态属性的约束

/**
 * 约束构造函数有指定静态属性
 */
type Constructor = {
  new (...args: any[]): {}; // 约束该类型允许通过new关键创建实例
  age: number; // 约束该类中必须有静态的属性age
};
function demo(fn: Constructor) {}
class Person {
  static age: number;
}
demo(Person); // demo允许接收Person类,Person类符合Constructor约束

替换被装饰的类

对于高级的一些装饰器,不仅仅是覆盖原型上的方法,还能添加新的方法和状态

需求: 设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取实例的创建时间

type Constructor = new (...args: any[]) => {};
function LogInfo<T extends Constructor>(target: T) {
  return class extends target {
    public createTime: Date;
    constructor(...args: any[]) {
      super(...args);
      this.createTime = new Date();
    }
    getTime() {
      console.log(`当前实例对象创建时间: ${this.createTime}`);
    }
  };
}
@LogInfo
class Person {
  constructor(public name: string, public age: number) {}
}
interface Person {
  getTime: () => void;
}
let p = new Person("张三", 18);
p.getTime(); // 输出: 当前实例对象创建时间: Wed Jul 23 2025 14:45:01 GMT+0800 (中国标准时间)

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,更加灵活控制装饰器的行为

需求: 定义一个LogInfo装饰器工厂,实现Person类的实例调用到info方法,且info输出内容的次数由LogInfo装饰器工厂的参数决定

type Constructor = new (...args: any[]) => {};
function LogInfo(count: number) {
  return function LogInfoD<T extends Constructor>(target: T) {
    target.prototype.info = function () {
      for (let i = 0; i < count; i++) {
        console.log(`我是装饰器工厂返回的装饰器方法:info, 第${i}次`);
      }
    };
  };
}
@LogInfo(5)
class Person {}
interface Person {
  info(): void;
}
let p = new Person();
console.log(p.info());

img

LogInfo(5): 就是一个装饰器工厂,返回一个装饰器函数,装饰器函数中可以使用装饰器工厂中的参数


装饰器组合

装饰器组合就是将多个装饰器组合在一起,装饰器组合的顺序很重要,装饰器组合的顺序是从下到上执行,从右向左执行
如果组合装饰器中存在装饰工厂,那么先调用装饰工厂函数发回装饰器,调用装饰器工厂顺序仍然是从下到上执行
通过装饰器工厂获取的装饰器参与到整个装饰器执行过程,并不会打乱装饰器执行的顺序

function LogInfoFactory1() {
  return function LogInfo(target: Function) {
    console.log(`我是LogInfoFactory1`);
  };
}
function LogInfoFactory3() {
  return function LogInfo(target: Function) {
    console.log(`我是LogInfoFactory3`);
  };
}
function LogInfo2(target: Function) {
  console.log(`我是LogInfo2`);
}
function LogInfo4(target: Function) {
  console.log(`我是LogInfo4`);
}
@LogInfo4
@LogInfoFactory3()
@LogInfo2
@LogInfoFactory1()
class Person {}

img


属性装饰器

基本语法

属性装饰器用来装饰类中普通属性和静态属性
语法: 在属性前面使用注解形式装饰,@装饰器函数名 属性

function Demo(target: object, propertyKey: string) {
  console.log("属性装饰器接收到参数,参数1:", target, "参数2:", propertyKey);
}
class Person {
  @Demo name: string = "张三";
  @Demo static age: number = 18;
}

对于装饰了普通属性时,装饰器接收到的第一个参数为成员所属类的原型对象
对于装饰了静态属性时,装饰器接收到的第一个参数为成员所属的类本身,就是一个class
不管是普通属性还是静态属性,装饰器收到的第二个参数始终为装饰的属性名
img


属性遮蔽问题

属性遮蔽: 类本身和类原型上都具有相同的属性时,在实例化类时,调用构造方法进行初始化属性值时将会初始化类原型上属性,而不会初始化类中声明的属性

class Person {
  age: number = 18;
  name: string;
  constructor(name: string) {
    this.name = name; // this.name实际上修改了原型上的name属性
  }
}
let value = "李四";
Object.defineProperty(Person.prototype, "name", {
  get() {
    return value;
  },
  set(newVal) {
    console.log('set原型属性值');
    value = newVal;
  },
});
let p = new Person('张三');
console.log(p);
console.log(p.name); // 实例中的name,仍然读取的原型上的name属性
console.log(Person.prototype.name); // 原型上的name属性

由于在new Person调用构造前,进行了对name数据劫持,name在调用构造方法时,将会对Person原型上的name进行赋值,而不是对实例本身的name属性进行赋值
img


应用举例

需求: 定义一个State属性装饰器,来监视属性的修改

function State(target: object, propertyKey: string) {
  /**
   * 定义实例上的属性与类中的属性进行关联,将该属性放到实例中 
   * 作用: 
      1. 防止set中直接操作propertyKey产生递归调用而栈内存溢出  
      2. 将该属性添加到实例对象中,隔离propertyKey的数据
   */
  let key = `_${propertyKey}`;
  Object.defineProperty(target, propertyKey, {
    get() {
      return this[key];
    },
    set(newVal) {
      console.log(`修改了属性${propertyKey}的值,新值: ${newVal}`);
      this[key] = newVal;
    },
  });
}
class Person {
  @State name: string;
  constructor(name: string) {
    this.name = name;
  }
}
let p = new Person("张三");
console.log(p.name);
p.name = '李四';
console.log(p.name);

img

如果在Object.defineProperty的set方法中直接操作propertyKey,则不断调用set方法,导致栈内存溢出

Object.defineProperty(target, propertyKey, {
  get() {
    return this[propertyKey];
  },
  set(newVal) {
    console.log(`修改了属性${propertyKey}的值,新值: ${newVal}`);
    this[propertyKey] = newVal;
  },
});

img

定义实例上的属性与类中的属性进行关联,将该属性放到实例中
作用:
1.防止set中直接操作propertyKey产生递归调用而栈内存溢出
2.将该属性添加到实例对象中,隔离propertyKey的数据,否则不同实例的在读取property值时将读取相同的key值,如下代码

function State(target: object, propertyKey: string) {
  let key:any;
  Object.defineProperty(target, propertyKey, {
    get() {
      return key;
    },
    set(newVal) {
      console.log(`修改了属性${propertyKey}的值,新值: ${newVal}`);
      key = newVal;
    },
  });
}
class Person {
  @State name: string;
  constructor(name: string) {
    this.name = name;
  }
}
let p = new Person("张三");
let p2 = new Person("李四");
console.log(p.name); // 输出: 李四
console.log(p2.name); // 输出: 李四

方法装饰器

方法装饰器: 用来装饰类中普通方法,静态方法

基本语法

装饰普通方法时,方法装饰器函数接收三个参数:
1.target: 方法所在的类原型对象
2.propertyKey: 方法名
3.descriptor: 方法的属性描述符对象,其类型为interface定义PropertyDescriptor类型,该对象中value就是方法本身

function Demo(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(
    "target:",
    target,
    "propertyKey:",
    propertyKey,
    "descriptor:",
    descriptor
  );
}
class Person {
  constructor(public name: string, public age: number) {}
  @Demo
  speak() {
    console.log(`${this.name} is ${this.age} years old.`);
  }
}

img


装饰静态方法时,方法装饰器函数接收三个参数:
1.target: 方法所在的类本身
2.propertyKey: 方法名
3.descriptor: 方法的属性描述符对象,其类型为interface定义PropertyDescriptor类型,该对象中value就是方法本身

function Demo(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log("target:", target);
  console.log("propertyKey:", propertyKey);
  console.log("descriptor:", descriptor)
}
class Person {
  constructor(public name: string, public age: number) {}
  speak() {
    console.log(`${this.name} is ${this.age} years old.`);
  }
  @Demo
  static say() {
    console.log("static method");
  }
}

img


应用举例

需求: 定义一个Logger方法装饰器,用于在方法执行前和执行后,扩展功能

function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
  // 获取原始方法,避免直接调用原始对象而产生的递归调用
  let originalMethod = descriptor.value;
  // 覆盖原始方法
  descriptor.value = function (...args: any[]) {
    console.log("before method");
    // 调用原始方法,保持原始方法中this指向实例
    originalMethod.call(this, ...args);
    console.log("after method");
  }
}
class Person {
  constructor(public name: string, public age: number) {}
  @Demo
  speak() {
    console.log(`${this.name} is ${this.age} years old.`);
  }
  static say() {
    console.log("static method");
  }
}
new Person("Tom", 18).speak();

img


需求: 定义一个Validate方法装饰器,用于验证数据的合法性

interface Adult {
  age: number;
}
interface IValidateRule {
  age: boolean;
}

function Validate(validateRule: IValidateRule) {
  return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    let originalMethod = descriptor.value;
    // 覆盖原始方法
    descriptor.value = function (...args: any[]) {
      console.log(args[0]);
      if (validateRule.age) {
        if (args[0].age) {
          if (args[0].age < 18) {
            console.log("未成年");
            throw new Error("未成年");
          } else {
            // 调用原始方法,保持原始方法中this指向实例
            originalMethod.apply(this, args);
          }
        }
      }
    };
  };
}
let validateRule: IValidateRule = {
  age: true,
};
class Person {
  constructor(public name: string, public age: number) {}
  @Validate(validateRule)
  static needAdult(person: Adult) {
    console.log("只接收成年人");
  }
}
Person.needAdult({ age: 17 });

img


访问器装饰器

访问器装饰器: 拦截访问和修改类属性的getter和setter方法

基本语法

function Validate(name: string) {
  return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('target', target);
    console.log('propertyKey', propertyKey);
    console.log('descriptor', descriptor);
  };
}
class Person {
  constructor(public name: string) {}
  get _name(){
    return this.name
  }
  @Validate('张')
  set _name(value){
    this.name = value
  }
}

在get、set方法上使用装饰器工厂进行自定义校验规则
装饰器接收的第三个参数descriptor中包含了get和set方法,因此可以对get、set方法进行装饰逻辑
img


需求: 设置类实例中姓名时,必须满足姓名包含

function Validate(name: string) {
  return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const _set = descriptor.set;
    // 覆盖set方法
    descriptor.set = function (value: string) {
      if (![...value].includes(name)) {
        throw new Error(`请输入正确的名字,必须包含${name}`);
      }
      if (_set) {
        console.log("setter");
        _set.call(this, value);
      }
    };
  };
}
class Person {
  constructor(public name: string) {}
  get _name() {
    return this.name;
  }
  @Validate("张")
  set _name(value) {
    this.name = value;
  }
}
let p = new Person("三"); // 没有调用set方法,因此允许设置成功
p._name = '三'; // 调用set方法,因此会抛出错误

img


参数装饰器

参数装饰器: 拦截类方法的参数,用于对参数进行校验。

基本语法

/*
 参数说明:
 ○ target:
 1.如果修饰的是【实例⽅法】的参数,target 是类的【原型对象】。
 2.如果修饰的是【静态⽅法】的参数,target 是【类】。
 ○ propertyKey:参数所在的⽅法的名称。
 ○ parameterIndex: 参数在函数参数列表中的索引,从 0 开始。
*/
function Demo(target: object, propertyKey: string, parameterIndex: number) {
 console.log(target)
 console.log(propertyKey)
 console.log(parameterIndex)
}
// 类定义
class Person {
 constructor(public name: string) { }
 speak(@Demo message1: any, mesage2: any) {
 console.log(`${this.name}想对说:${message1},${mesage2}`);
 }
}

应用举例

需求: 定义⽅法装饰器 Validate ,同时搭配参数装饰器 NotNumber ,来对 speak ⽅法的参数类型进⾏限制

参数装饰器执行优先级高于方法装饰器

function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
  console.log('方法参数装饰器');
  // 初始化或获取当前⽅法的参数索引列表
  let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
  // 将当前参数索引添加到列表中
  notNumberArr.push(parameterIndex);
  // 将列表存储回⽬标对象
  target[`__notNumber_${propertyKey}`] = notNumberArr;
}
// ⽅法装饰器定义
function Validate(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log('方法装饰器');
  const method = descriptor.value;
  descriptor.value = function (...args: any[]) {
    // 获取被标记为不能为空的参数索引列表
    const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
    // 检查参数是否为 null 或 undefined
    for (const index of notNumberArr) {
      if (typeof args[index] === "number") {
        throw new Error(
          `⽅法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!`
        );
      }
    }
    // 调⽤原始⽅法
    return method.apply(this, args);
  };
  return descriptor;
}
// 类定义
class Student {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @Validate
  speak(@NotNumber message1: any, mesage2: any) {
    console.log(`${this.name}想对说:${message1},${mesage2}`);
  }
}
// 使⽤
const s1 = new Student("张三");
s1.speak(100, 200);

参数装饰器先执行,收集不能为数字的参数脚标
当调用speak方法时,方法装饰器会根据已经收集了不能为数字的参数脚标对参数进行校验,如果参数类型为数字,则抛出错误
img

posted @ 2025-07-15 16:26  ethanx3  阅读(36)  评论(0)    收藏  举报