Administrator
Administrator
发布于 2025-08-18 / 1 阅读

TypeScript 速通

TypeScript 速通

Typescript 简介

TypeScript 是由微软开发的,基于 JavaScript 的一个拓展语言

  1. TypeScript 包含了 Javascript 的所有内容,即 TypeScript 是 JavaScript 的超集
  2. TypeScript 增加了静态类型检查、接口、泛型等多种现代开发特性,更适合大型项目的开发
  3. TypeScript 需要编译为 JavaScript,如何交给浏览器或其他 JavaScript 运行环境执行

为什么需要 TypeScript

JavaScript 今非昔比

  1. JavaScript 当年诞生时的定位时浏览器脚本语言,用于在网页中嵌入一些简单的逻辑,而且代码量很少
  2. 随着时间的推移,JavaScript 变得越来越流行,如今的 JavaScript 已经可以全栈编程
  3. 现如今的 JavaScript 应用场景比当年丰富的多,代码量也比当年很多,随便一个 JavaScript 项目代码量可以轻松到达几万行甚至几十万行
  4. 然而 JavaScript 当年出生简陋,没有考虑到如今的应用场景和代码量,逐渐出现了很多困扰

JavaScript 中的困扰

1. 不清不楚的数据类型
let welcome = 'hello'
welcome() // 错误的数据类型

2. 有漏洞的逻辑
const str = Date.now() % 2 ? '奇数' : '偶数'

if (str !== '奇数') {
  alert('hello')
} else if (str === '偶数') {
  alert('world') // 不可能到达此行
}

3. 访问不存在的属性
const obj = { width: 10, height: 15 }
const area = obj.width * obj.heightTow // 调用了不存在的属性

4. 低级的拼写错误
const message = 'hello world'
message.toUperCase() // 少了个p

TypeScript 静态类型检查

在代码运行前进行检查,发现代码的错误和不合理之处,运行时异常的出现几率,此种检查叫静态类型检查,TypeScript 的核心就是静态类型检查,简言之就是把运行时的错误前置。同样的功能,TypeScript 的代码量要大于 JavaScript,但是由于 TypeScript 的代码结构更加清晰,在后期代码的维护中 TypeScript 远胜于 JavaScript

编译 TypeScript

浏览器不能直接运行 TypeScript,需要编译为 JavaScript 再交由浏览器解析器执行。

1. 命令行编译

命令行编译可以不写.ts,编译好后生成同名的.js文件这个方法效率低下,一般不使用

tsc index.ts

2. 自动化编译

init后会生成tsconfig.json文件,在这里可以设置编译 JavaScript 的版本、严格模式等,同时建议打开配置文件中的noEmitOnError,避免后续出现错误的代码也被编译为.js文件。

watch不写监视的文件就是监视全部 ts 文件的变化,监测到.ts文件变化自动编译为.js

tsc --init
tsc --watch index.ts # 或tsc -w

类型声明

TypeScript 使⽤:来对变量或函数形参,进⾏类型声明:

let a: string // 变量a只能存储字符串类型,不能是包装类,TS官方推荐写法
let b: number // 变量b只能存储数值类型
let c: boolean // 变量c只能存储布尔值

a = 'hello'
b = -10
c = true

// 参数x必须是数字类型,参数y也必须是数字类型,函数返回值也必须是数字类型
// 参数只能是2个,不能是1个或者3个
function count(x: number, y: number): number {
  return x + y
}

let result = count(10, 20)
console.log(result)

:后也可以写字⾯量类型,不过实际开发中⽤的不多

let x: '你好' //a的值只能为字符串"你好",不能为其他
let y: 100 //b的值只能为数字100,不能为其他

注意:

大写的StringNumberBoolean表示的是 new 出来的包装对象,变量x可以是包装对象或基本类型,但不推荐这么写,内存不友好

let x: String // 非推荐写法
let y: Number // 非推荐写法
let z: Boolean // 非推荐写法

类型推断

如果不写: 数据类型进行类型声明,TypeScript 也会根据我们书写的情况进行推断,并规范数据类型,但面对复杂的数据类型可能会推断出错,因此建议全部进行类型声明

let d = -1 //TypeScript会推断出变量d的类型是数字
d = false // 这里就出错了

类型总览

JavaScript 中的数据类型

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

TypeScript 中的数据类型

  1. JavaScript 中的 8 个数据类型
  2. 6 个新类型
    • any
    • unknow
    • never
    • void
    • tuple
    • enum
  3. 2 个用于自定义类型的方式
    • type
    • interface

常用类型与语法

1. any

any表示可以是任意类型,⼀旦将变量类型限制为any,那就意味着放弃了对该变量的类型检查。

// 【显式的any】明确的表示a的类型是any
let a: any
// 以下对a的赋值,均⽆警告
a = 100
a = '你好'
a = false

// 【隐式的any】没有明确的表示b的类型是any,但TS主动推断出来b是any
let b
//以下对b的赋值,均⽆警告
b = 100
b = '你好'
b = false

坑:any类型的变量,可以赋值给任意类型的变量,且不会产生警告,这会破坏掉原来的类型检查

let c:any
c = 9
let x: string
x = c // ⽆警告
x = 9 // 警告

2. unknow

unknown表示未知类型

  1. 其可以理解为一个类型安全的any,适用于不确定的数据类型
// 设置a的类型为unknown
let a: unknown
//以下对a的赋值,均符合规范
a = 99
a = false
a = '你好'

// 设置x的数据类型为string,和any的区别在这里,类型安全
let x: string
x = a  // 警告:不能将类型“unknown”分配给类型“string”

  1. unknown会强制开发者在使⽤之前进⾏类型检查,从而提供更强的类型安全性。
// 设置a的类型为unknown
let a: unknown
a = 'hello'
//第⼀种⽅式:加类型判断
if(typeof a === 'string'){
 x = a
 console.log(x)
}
//第⼆种⽅式:加断⾔
x = a as string
//第三种⽅式:加断⾔
x = <string>a

  1. 读取any类型数据的任何属性都不会报错,unknown正好与之相反。
let str1: string
str1 = 'hello'
str1.toUpperCase() //⽆警告

let str2: any
str2 = 'hello'
str2.toUpperCase() //⽆警告

let str3: unknown
str3 = 'hello'
str3.toUpperCase()//警告:“str3”的类型为“未知”

(str3 as string).toUpperCase() // 使⽤断⾔强制指定str3的类型为string,⽆警告

3. never

never的含义是:任何值都不是,即:不能有值,例如undefinednull''0都不行

  1. 几乎不用 never 去直接限制变量,因为没有意义,例如:
// 指定a的类型为never, 那就意味着a以后不能存任何的数据了
let a: never
// 以下对a的所有赋值都会有警告
a = 1
a = true
a = undefined
a = null

  1. never 一般是 TypeScript 主动推断出来的,例如: 把鼠标放在a上提示a为 never
// 指定a的类型为string
let a: string
// 给a设置⼀个值
a = 'hello'
if (typeof a === 'string') {
 console.log(a.toUpperCase())
} else {
 console.log(a) // TypeScript会推断出此处的a是never,因为没有任何⼀个值符合此处的逻辑
}

  1. never也可⽤于限制函数的返回值,返回never的函数不能具有可访问的终结点,即不可能正常结束
// 限制throwError函数不需要有任何返回值,任何值都不⾏,像undeifned、null都不⾏
function throwError(str: string): never {
 throw new Error('程序异常退出:' + str)
}

4. void

void即空,通常用于函数返回值,函数不反悔任何值,调用者也不依赖其返回值进行任何操作

  1. void通常用于函数返回值声明
function logMessage(msg:string):void{
 console.log(msg)
}
logMessage('hello world')

因为没有编写return指定函数返回值,所以logMessage函数是没有显式返回值的,但会有⼀个隐式返回值undefined,虽然函数返回类型为void,但也是可以接受undefined的,即:undefinedvoid可以接受的⼀种 “空”。因此以下写法均规范:

// 不写return⽆警告
function logMessage(msg:string):void{
 console.log(msg)
}
// 只写return⽆警告
function logMessage(msg:string):void{
 console.log(msg)
 return;
}
// return undefined⽆警告
function logMessage(msg:string):void{
 console.log(msg)
 return undefined
}

  1. 限制函数返回值时voidundefined的区别:调用者也不依赖其返回值进行任何操作
function logMessage(msg:string):void{
 console.log(msg)
}
let result = logMessage('hello world')
if(result){ // 此⾏报错:⽆法测试 "void" 类型的表达式的真实性
 console.log('logMessage有返回值')
}

function logMessage(msg:string):undefined{
 console.log(msg)
}
let result = logMessage('hello world')
if(result){ // 此⾏⽆警告
 console.log('logMessage有返回值')
}

也就是说即使我们知道void返回的时undefined我们也不应该关心函数返回值的问题,不应该依赖返回值进行任何操作

5. object

objectObject在实际开发中使用相对较少,因为范围太大了

小写object

表示所有非原始类型,可存储对象、函数、数组等

let a: object
// 以下代码,是将⾮原始类型赋给a,所以均符合要求
a = {}
a = { name: '张三' }
a = [1, 3, 5, 7, 9]
a = function () {}
a = new String('123')
class Person {}
a = new Person()

// 以下代码,是将原始类型赋给a,有警告
a = 1
a = true 
a = 'hello world'
a = null
a = undefined

大写Object

所有可以调用大Object方法的类型,即除了undefinednull以外的任何值,只要沿着原原型链能找到Object上的方法即可,包括几个基本类型。

//b的值必须是Object的实例对象(除去undefined和null的任何值)
let b:Object 
// Object的实例对象,无警告
b = {}
b = {name:'张三'}
b = [1,3,5,7,9]
b = function(){}
b = new String('123')
class Person {}
b = new Person()

// 本身不是Object的实例对象,但其包装对象是Object的实例,无警告
b = 1 
b = true 
b = 'hello world'

// 以下代码均有警告
b = null
b = undefined

可以看出,大写Object比小写object范围更加广泛,因此范围太大了,实际开发中使用频率极低

声明对象类型

  1. 实际开发中,限制一般对象,通常使用以下形式
// 限制person1对象必须有name属性,age为可选属性
let person1: { name: string; age?: number }
// 含义同上,也能⽤分号做分隔
let person2: { name: string; age?: number }
// 含义同上,也能⽤换⾏做分隔
let person3: {
  name: string
  age?: number
}
// 如下赋值均可以
person1 = { name: '李四', age: 18 }
person2 = { name: '张三' }
person3 = { name: '王五' }
// 如下赋值不合法,因为person3的类型限制中,没有对gender属性的说明
person3 = { name: '王五', gender: '男' }

  1. 索引签名:允许定义对象可以具有任意数量的属性,这些属性的键值类型可变,常⽤于描述类型不确定的属性(具有动态属性的对象)
// 限制person对象必须有name属性,可选age属性但值必须是数字,同时可以有任意数量、任意类型的其他属性
let person: {
  name: string
  age?: number
  [key: string]: any // 索引签名,完全可以不⽤key这个单词,换成其他的也可以,只需要保证key为字符串,值为任何值
}
// 赋值合法
person = {
  name: '张三',
  age: 18,
  gender: '男',
  city: '深圳'
}

声明函数类型

let count: (a: number, b: number) => number // 限制count有两个数字类型的参数,返回值也是数字类型的,这里=>是分隔符
// 这里定义函数时就可以省略书写参数和返回值类型检查了
count = function (x, y) {
  return x + y
}

函数类型声明还可以使用接口、自定义类型等方式,后面详细讲解

声明数组类型

let arr1: string[]
let arr2: number[]
let arr3: Array<string> // 泛型

arr1 = ['a', 'b', 'c']
arr2 = [1, 2, 3]
arr3 = ['hello', 'world']

6. tuple

元组 Tuple 是⼀种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的且可以不同。元组用于精确描述⼀组值的类型,? 表示可选元素

// 第⼀个元素必须是 string 类型,第⼆个元素必须是 number 类型。
let arr1: [string, number]
// 第⼀个元素必须是 number 类型,第⼆个元素是可选的,如果存在,必须是 boolean 类型。
let arr2: [number, boolean?]
// 第⼀个元素必须是 number 类型,后⾯的元素可以是任意数量的 string 类型
let arr3: [number, ...string[]]
// 可以赋值
arr1 = ['hello', 123]
arr2 = [100, false]
arr2 = [200]
arr3 = [100, 'hello', 'world']
arr3 = [100]
// 不可以赋值,arr1声明时是两个元素,赋值的是三个
arr1 = ['hello', 123, false]

7. enum

枚举 Enum 可以定义一组命名常量,它能增强代码的可读性,也让代码更好维护。

如下代码的功能是:根据调⽤walk时传⼊的不同参数,执⾏不同的逻辑,存在的问题是调⽤walk时传参时没有任何提示,开发者很容易写错字符串内容,并且用于逻辑判断的updownleftright是连续且相关的⼀组值,那此时就特别适合使⽤枚举(enum)

// 定义移动函数
function walk(str: string) {
  if (str === 'up') {
    console.log('向【上】⾛')
  } else if (str === 'down') {
    console.log('向【下】⾛')
  } else if (str === 'left') {
    console.log('向【左】⾛')
  } else if (str === 'right') {
    console.log('向【右】⾛')
  } else {
    console.log('未知⽅向')
  }
}
// 调用移动函数
walk('up')
walk('down')
walk('left')
walk('right')

数字枚举

数字枚举⼀种最常见的枚举类型,其成员的值会自动递增,且数字枚举还具备反向映射的特点,在下面代码的打印中,不难发现:可以通过值来获取对应的枚举成员名称

// 定义⼀个描述上下左右⽅向的枚举Direction
enum Direction { // 一般首字母大写
  Up,
  Down,
  Left,
  Right,
}
console.log(Direction) // 打印 {0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right', Up: 0, Down: 1, Left: 2, Right: 3}

// 反向映射
console.log(Direction.Up) // 0
console.log(Direction[0]) // Up

// 此⾏代码报错,枚举中的属性是只读的
Direction.Up = 'shang'

也可以指定枚举成员的初始值,其后的成员值会自动递增。

enum Direction {
  Up = 6,
  Down,
  Left,
  Right,
}
console.log(Direction.Up) // 输出: 6
console.log(Direction.Down) // 输出: 7

使用枚举之后案例的代码可以变得更加直观可读,类型更加安全,同时易于维护。

// 定义⼀个描述上下左右⽅向的枚举Direction
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
// 定义移动函数
function walk(n: Direction) {
  if (n === Direction.Up) {
    console.log('向【上】⾛')
  } else if (n === Direction.Down) {
    console.log('向【下】⾛')
  } else if (n === Direction.Left) {
    console.log('向【左】⾛')
  } else if (n === Direction.Right) {
    console.log('向【右】⾛')
  } else {
    console.log('未知⽅向')
  }
}
// 调用移动函数
walk(Direction.Up)
walk(Direction.Down)

字符串枚举

枚举成员的值是字符串

enum Direction {
  Up = 'up',
  Down = 'down',
  Left = 'left',
  Right = 'right',
}
let dir: Direction = Direction.Up
console.log(dir) // 输出: "up"

常量枚举

官方描述:常量枚举是一种特殊枚举类型,它使⽤const关键字定义,在编译时会被内联避免生成一些额外的代码

何为编译时被内联?所谓 “内联” 其实就是 TypeScript 在编译时,会将枚举成员引用替换为它们的实际值,而不是生成额外的枚举对象。这可以减少生成的 JavaScript 代码量,并提高运行时效性能

还是看一下之前的例子:

enum Directions {
  Up,
  Down,
  Left,
  Right,
}
let x = Directions.Up

在不加const时被编译为:

"use strict";
var Directions;
(function (Directions) {
    Directions[Directions["Up"] = 0] = "Up";
    Directions[Directions["Down"] = 1] = "Down";
    Directions[Directions["Left"] = 2] = "Left";
    Directions[Directions["Right"] = 3] = "Right";
})(Directions || (Directions = {}));
let x = Directions.Up;

加了const后被编译为:

"use strict";
let x = 0 /* Directions.Up */;

8. type

type 可以为任意类型创建别名,让代码更简洁、可读性更强,同时能更方便地进行类型复用和扩展。

基本用法

类型别名使用 type 关键字定义,type 后跟类型名称,例如下面代码中 num 是类型别名:

type num = number
let price: num
price = 100

联合类型

联合类型是一种高级类型,它表示一个值可以是几种不同类型之一。通过|定义。

type Status = number | string // Status 可以时number类型也可以时string类型
type Gender = '男' | '⼥' // Gender只能是两种字符串'男'或'女'

function printStatus(status: Status) {
  console.log(status)
}
function logGender(str: Gender) {
  console.log(str)
}

printStatus(404)
printStatus('200')
printStatus('501')
logGender('男')
logGender('⼥')
logGender('未知') // 出现警告

交叉类型

交叉类型(Intersection Types)允许将多个类型合并为一个类型。合并后的类型将拥有所有被合并类型的成员。交叉类型通常用于对象类型。通过&定义。

//⾯积
type Area = {
  height: number //⾼
  width: number //宽
}
//地址
type Address = {
  num: number //楼号
  cell: number //单元号
  room: string //房间号
}

// 定义类型House,且House是Area和Address组成的交叉类型
type House = Area & Address
const house: House = {
  height: 180,
  width: 75,
  num: 6,
  cell: 3,
  room: '702',
}

一个特殊情况

先来看看以下两行段代码:

在函数定义时,限制函数返回值为void,那么函数的返回值就必须是空。

function demo(): void {
  // 返回undefined合法
  return undefined
    
  // 以下返回均不合法
  return 100
  return false
  return null
  return []
}
demo()


使用类型声明限制函数返回值为void时,TypeScript 并不会严格要求函数返回空

type LogFunc = () => void // LogFunc类型的函数:不接收参数、返回值为void
const f1: LogFunc = () => {
  return 100 // 允许返回⾮空值
}
const f2: LogFunc = () => 200 // 允许返回⾮空值
const f3: LogFunc = function () {
  return 300 // 允许返回⾮空值
}

为什么会这样?

是为了确保如下代码成立,我们知道Array.prototype.push的返回值是一个数字,而Array.prototype.forEach方法期望其回调的返回类型是void。不过就算拿到了返回值你也不能拿返回值做任何操作,会报错

const src = [1, 2, 3]
const dst = [0]
src.forEach(el => dst.push(el)) // 这里要求返回的是void,但是箭头省略了`{}`不一定为void

9. 接口

见类后面部分

这里和 Java、C# 中的类高度相似,可以直接到接口

TypeScript 中的类

class Person {
  // 属性声明,在typescript中必须先进行属性声明
  name: string
  age: number
  // 构造器
  constructor(name: string, age: number) {
    // 这里类型声明也是必须的
    this.name = name
    this.age = age
  }
  // ⽅法
  speak() {
    console.log(`我叫:${this.name},今年${this.age}岁`)
  }
}
// Person实例
const p1 = new Person('周杰伦', 38)

类的继承

// 继承
class Student extends Person {
  grade: string
  // 构造器
  constructor(name: string, age: number, grade: string) {
    // 本例中若Student类不需要额外的属性,Student的构造器可以省略
    super(name, age)
    this.grade = grade
  }

  // 重写从⽗类继承的⽅法
  override speak() { // 这里加override后如果拼写错误会报错
    console.log(`我是学⽣,我叫:${this.name},今年${this.age}岁,在读${this.grade}年级`)
  }
  // ⼦类⾃⼰的⽅法
  study() {
    console.log(`${this.name}正在努⼒学习中......`)
  }
}

属性修饰符

四种属性修饰符

修饰符含义具体规则
public公开的可以被:类内部、子类、类外部访问
protected受保护的可以被:类内部、子类访问
private私有的可以被:类内部访问
readonly只读属性属性无法修改

属性的简写形式

简写前:

class Person {
  public name: string
  public age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

简写后:

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

public 修饰符

// 父类
class Person {
  // name写了public修饰符,age没写修饰符,最终都是public修饰符
  public name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
  speak() { // 这里方法没写修饰符,也是public
    // 类的【内部】可以访问public修饰的name和age
    console.log(`我叫:${this.name},今年${this.age}岁`)
  }
}
const p1 = new Person('张三', 18)
// 类的【外部】可以访问public修饰的属性
console.log(p1.name)

// 子类
class Student extends Person {
  constructor(name: string, age: number) {
    super(name, age)
  }
  study() {
    // 【⼦类中】可以访问⽗类中public修饰的:name属性、age属性
    console.log(`${this.age}岁的${this.name}正在努⼒学习`)
  }
}

protected 修饰符

// 父类
class Person {
  // name和age是受保护属性,不能在类外部访问,但可以在【类】与【⼦类】中访问
  constructor(protected name: string, protected age: number) {}
  // getDetails是受保护⽅法,不能在类外部访问,但可以在【类】与【⼦类】中访问
  protected getDetails(): string {
    // 类中能访问受保护的name和age属性
    return `我叫:${this.name},年龄是:${this.age}`
  } 
  introduce() { // introduce是public⽅法,类、⼦类、类外部都能使⽤
    // 类中能访问受保护的getDetails⽅法
    console.log(this.getDetails())
  }
}
const p1 = new Person('杨超越', 18)
// 可以在类外部访问introduce
p1.introduce()
// 以下代码均报错
// p1.getDetails()
// p1.name
// p1.age


// 子类
class Student extends Person {
  constructor(name: string, age: number) {
    super(name, age)
  }
  study() {
    // ⼦类中可以访问introduce
    this.introduce()
    // ⼦类中可以访问name
    console.log(`${this.name}正在努⼒学习`)
  }
}
const s1 = new Student('tom', 17)
s1.introduce()

private 修饰符

class Person {
  constructor(
    public name: string,
    public age: number,
    // IDCard属性为私有的(private)属性,只能在【类内部】使⽤
    private IDCard: string
  ) {}
  private getPrivateInfo() {
    // 类内部可以访问私有的(private)属性 —— IDCard
    return `身份证号码为:${this.IDCard}`
  }
  getInfo() {
    // 类内部可以访问受保护的(protected)属性 —— name和age
    return `我叫: ${this.name}, 今年刚满${this.age}岁`
  }
  getFullInfo() {
    // 类内部可以访问公开的getInfo⽅法,也可以访问私有的getPrivateInfo⽅法
    return this.getInfo() + ',' + this.getPrivateInfo()
  }
}
const p1 = new Person('张三', 18, '110114198702034432')
console.log(p1.getFullInfo())
console.log(p1.getInfo())
// 以下代码均报错
// p1.name
// p1.age
// p1.IDCard
// p1.getPrivateInfo()

readonly 修饰符

class Car {
 constructor(
 public readonly vin: string, //⻋辆识别码,为只读属性
 public readonly year: number,//出⼚年份,为只读属性
 public color: string,
 public sound: string
 ) { }
 // 打印⻋辆信息
 displayInfo() {
 console.log(`
 识别码:${this.vin},
 出⼚年份:${this.year},
 颜⾊:${this.color},
 ⾳响:${this.sound}
 `);
 }
}
const car = new Car('1HGCM82633A123456', 2018, '⿊⾊', 'Bose⾳响');
car.displayInfo()
// 以下代码均错误:不能修改 readonly 属性
// car.vin = '897WYE87HA8SGDD8SDGHF'; 
// car.year = 2020;

抽象类

概述:抽象类是一种无法被实例化的类,专门用来定义类的结构和行为,类中可以写抽象方法,也可以写具体实现。抽象类主要用来为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法。

简记:抽象类不能实例化,其意义是可以被继承,抽象类里可以有普通方法、也可以有抽象方法

通过以下场景理解抽象类:

我们定义一个抽象类Package,表示所有包裹的基本结构,任何包裹都有重量属性weight,包裹都需要计算运费。但不同类型的包裹(如:标准包裹、特快专递)都有不同的运费计算方式,因此用于计算运费的calculate方法是一个抽象方法,必须由具体的子类来实现。

// Package类
abstract class Package {
  constructor(public weight: number) {}
  // 抽象⽅法:⽤来计算运费,不同类型包裹有不同的计算⽅式
  abstract calculate(): number
  // 通⽤⽅法:打印包裹详情
  printPackage() {
    console.log(`包裹重量为: ${this.weight}kg,运费为: ${this.calculate()}元`)
  }
}

// 标准包裹StandardPackage继承了Package类,实现了calculate方法
class StandardPackage extends Package {
  constructor(
    weight: number,
    public unitPrice: number // 每公⽄的固定费率
  ) {
    super(weight)
  }
  // 实现抽象⽅法:计算运费
  calculate(): number {
    return this.weight * this.unitPrice
  }
}
// 创建标准包裹实例
const s1 = new StandardPackage(10, 5)
s1.printPackage()

// ExpressPackage特快专递继承了Package类,实现了calculate方法
class ExpressPackage extends Package {
  constructor(
    weight: number,
    private unitPrice: number, // 每公⽄的固定费率(快速包裹更⾼)
    private additional: number // 超出10kg以后的附加费
  ) {
    super(weight)
  }
  // 实现抽象⽅法:计算运费
  calculate(): number {
    if (this.weight > 10) {
      // 超出10kg的部分,每公⽄多收additional对应的价格
      return 10 * this.unitPrice + (this.weight - 10) * this.additional
    } else {
      return this.weight * this.unitPrice
    }
  }
}
// 创建特快专递实例
const e1 = new ExpressPackage(13, 8, 2)
e1.printPackage()

那么何时适合使用抽象类呢?

  1. 定义通用接口:为一组相关的类定义通用的行为(方法或属性)时。
  2. 提供基础实现:在抽象类中提供某些方法或为其提供基础实现,这样派生类就可以继承这些实现。
  3. 确保关键实现:强制派生类实现一些关键行为。
  4. 共享代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复。

接口 interface

interface是一种定义结构的方式,主要作用是为【类、对象、函数】等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意 interface只能定义格式,不能包含任何实现

定义类结构

// PersonInterface接⼝,⽤于限制Person类的格式
interface PersonInterface {
  name: string
  age: number
  speak(n: number): void
}

// 定义⼀个类 Person,实现 PersonInterface 接⼝
class Person implements PersonInterface {
  constructor(public name: string, public age: number) {}
  // 实现接⼝中的 speak ⽅法
  speak(n: number): void {
    for (let i = 0; i < n; i++) {
      // 打印出包含名字和年龄的问候语句
      console.log(`你好,我叫${this.name},我的年龄是${this.age}`)
    }
  }
}

// 创建⼀个 Person 类的实例 p1,传⼊名字 'tom' 和年龄 18
const p1 = new Person('tom', 18)
p1.speak(3)

定义对象结构

interface UserInterface {
  name: string
  readonly gender: string // 只读属性
  age?: number // 可选属性
  run: (n: number) => void
}
const user: UserInterface = {
  name: '张三',
  gender: '男',
  age: 18,
  run(n) {
    console.log(`奔跑了${n}⽶`)
  },
}

定义函数结构

interface CountInterface {
  (a: number, b: number): number // 两个number类型参数,返回值也是number类型的函数
}
const count: CountInterface = (x, y) => {
  return x + y
}

接口之间的继承

interface PersonInterface {
  name: string 
  age: number
}
// StudentInterface 继承了 PersonInterface 接口实现代码复用
interface StudentInterface extends PersonInterface {
  grade: string
}
const stu: StudentInterface = {
  name: '张三',
  age: 25,
  grade: '⾼三',
}

接口自动合并

以下代码重复定义了PersonInterface接口,PersonInterface接口自动合并了,必须实现两个接口中的全部属性和方法

// PersonInterface接⼝
interface PersonInterface {
  // 属性声明
  name: string
  age: number
}
// 给PersonInterface接⼝添加新属性
interface PersonInterface {
  // ⽅法声明
  speak(): void
}
// Person类实现PersonInterface
class Person implements PersonInterface {
  name: string
  age: number
  // 构造器
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
  // ⽅法
  speak() {
    console.log('你好!我是⽼师:', this.name)
  }
}

何时使用接口

  1. 定义对象的格式:描述数据模型、API 响应格式、配置对象等是开发中接口应用最多的场景。
  2. 类的契约:规定一个类需要实现哪些属性和方法。
  3. 自动和合并:一般用于扩展第三方库的类型,这种特性在大型项目中可能会用到。

一些相似的概念

1. interface 和 type

相同点:

  • interfacetype都可以用于定义对象结构,在定义对象结构时两者可以互换。

不同点:

  • interface:更专注于定义对象和类的结构,支持继承、合并

  • type:可以定义类型别名、联合类型、交叉类型,但不支持继承和自动合并。

两者都可以定义对象的结构:

// 使⽤ interface 定义 Person 对象
interface PersonInterface {
  name: string
  age: number
  speak(): void
}
// 使⽤ type 定义 Person 对象
type PersonType = {
  name: string
  age: number
  speak(): void
}
// 使⽤PersonInterface
/* let person: PersonInterface = {
 name:'张三',
 age:18,
 speak(){
 console.log(`我叫:${this.name},年龄:${this.age}`)
 }
} */
// 使⽤PersonType
let person: PersonType = {
  name: '张三',
  age: 18,
  speak() {
    console.log(`我叫:${this.name},年龄:${this.age}`)
  },
}

interface支持继承、合并

interface PersonInterface {
  name: string // 姓名
  age: number // 年龄
}
interface PersonInterface {
  speak: () => void
}
interface StudentInterface extends PersonInterface {
  grade: string // 年级
}
const student: StudentInterface = {
  name: '李四',
  age: 18,
  grade: '⾼⼆',
  speak() {
    console.log(this.name, this.age, this.grade)
  },
}

type交叉类型可以实现和interface继承、合并一样的效果,但看起来过于复杂,不方便

// 使⽤ type 定义 Person 类型,并通过交叉类型实现属性的合并
type PersonType = {
  name: string // 姓名
  age: number // 年龄
} & {
  speak: () => void
}
// 使⽤ type 定义 Student 类型,并通过交叉类型继承 PersonType
type StudentType = PersonType & {
  grade: string // 年级
}
const student: StudentType = {
  name: '李四',
  age: 18,
  grade: '⾼⼆',
  speak() {
    console.log(this.name, this.age, this.grade)
  },
}

2. interface 和抽象类

相同点:

  • interface和抽象类均可以用于定义一个类的格式(应遵循的契约)

不同点:

  • 接口只能描述结构,不能有任何实现代码,一个类可以实现多个接口
  • 抽象类既可以包含抽象方法,也可以包含具体方法,一个类只能继承一个抽象类

类实现多个接口:

// FlyInterface 接⼝
interface FlyInterface {
  fly(): void
}
// 定义 SwimInterface 接⼝
interface SwimInterface {
  swim(): void
}
// Duck 类实现了 FlyInterface 和 SwimInterface 两个接⼝
class Duck implements FlyInterface, SwimInterface {
  fly(): void {
    console.log('鸭⼦可以⻜')
  }
  swim(): void {
    console.log('鸭⼦可以游泳')
  }
}

// 创建⼀个 Duck 实例
const duck = new Duck()
duck.fly() // 输出: 鸭⼦可以⻜
duck.swim() // 输出: 鸭⼦可以游泳

泛型

泛型允许我们在定义函数、类或接口时,使⽤类型参数来表示未指定的类型,这些参数在具体使用时,才被指定具体的类型(可以不是基本类型,比如type声明的类型),泛型能让同一段代码适用于多种类型,同时仍然保持类型的安全性。

泛型函数

// 定义参数和返回值类型的时候我们不确定,使用泛型规定
function logData<T>(data: T): T {
  console.log(data)
  return data
}
// 在调用时,我们确定了具体的类型
logData<number>(100)
logData<string>('hello')

泛型可以有多个:

// 这里有两个泛型
function logData<T, U>(data1: T, data2: U): T | U {
  console.log(data1, data2)
  return Date.now() % 2 ? data1 : data2
}
logData<number, string>(100, 'hello')
logData<string, boolean>('ok', false)

泛型接口

interface PersonInterface<T> {
  name: string
  age: number
  extraInfo: T // 这里用到了泛型
}
let p1: PersonInterface<string>
let p2: PersonInterface<number>
p1 = { name: '张三', age: 18, extraInfo: '⼀个好⼈' }
p2 = { name: '李四', age: 18, extraInfo: 250 }

泛型约束

interface LengthInterface {
  length: number
}
// 约束规则是:传⼊的类型T必须具有 length 属性
function logPerson<T extends LengthInterface>(data: T): void {
  console.log(data.length)
}
logPerson<string>('hello')
// 以下行代码报错:因为number不具备length属性
logPerson<number>(100) 

泛型类

class Person<T> {
  constructor(
    public name: string, 
     public age: number, 
     public extraInfo: T // 这里用到了泛型
    ) {}
  speak() {
    console.log(`我叫${this.name}今年${this.age}岁了`)
    console.log(this.extraInfo)
  }
}
// 测试代码1
const p1 = new Person<number>('tom', 30, 250)
// 测试代码2
type JobInfo = { // 这里时typo声明的高级类型
  title: string
  company: string
}
const p2 = new Person<JobInfo>('tom', 30, { title: '研发总监', company: '发发发科技公司' })

类型声明文件

类型声明文件是 TypeScript 中的一种特殊文件,通常以.d.ts作为扩展名。它的主要作用是为现有的 JavaScript 代码提供类型信息,使得 TypeScript 能够在使用这些 JavaScript 库或模块时进行类型检查和提示

假设以下代码demo.js是我们引入的库,是用 JavaScript 写的:

// demo.js
export function add(a, b) {
  return a + b
}
export function mul(a, b) {
  return a * b
}

我们现在要引入这两个函数,到我们自己的 TypeScript 项目index.ts 中:

// index.ts
import { add, mul } from './demo.js' // 这里会报错,无法找到模块“./demo.js”的声明文件,“demo.js”隐式拥有 "any" 类型。
const x = add(2, 3)
const y = mul(4, 5)
console.log(x, y)

这是我们就要用到类型声明文件demo.d.ts

// demo.d.ts
declare function add(a: number, b: number): number;
declare function mul(a: number, b: number): number;
export { add, mul };

类型声明文件一般放在@types文件夹下,大多主流库都有支持,一般不需要开发者自己书写。

TypeScript 装饰器

虽然 TypeScript5.0 版本中可以直接使用类装饰器,但为了确保其他装饰器可用,现阶段使用时,仍建议使用experimentalDecorators配置来开启装饰器支持。

装饰器简介

  1. 装饰器自 2015 年在 ECMAScript6 中被提出,其本质是一种特殊的函数,它可以对类、属性、方法、参数进行扩展,同时能让代码更简洁。
  2. 截止目前,装饰器依然是实验性特性 ,需要开发者手动调整配置,来开启装饰器支持
  3. 装饰器有 5 种:装饰器、属性装饰器、方法装饰器、访问器装饰器、参数装饰器

类装饰器

1. 基本语法

// Demo装饰器
function Demo(target: any) {
  console.log(target)
}

@Demo // 打印 [class Person]
class Person {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

2. 应用举例

定义一个装饰器,实现Person实例调用toString时返回JSON.stringify的执行结果。

// 使用装饰器重写toString方法 + 封闭其原型对象
function CustomToString(target: Function) {
  // 向被装饰类的原型上添加自定义的 toString 方法
  target.prototype.toString = function () {
    return JSON.stringify(this) // 这里不能是target,因为target是类本身,而不是类的实例
  }
  // 封闭其原型对象,禁止随意操作其原型对象
  Object.seal(target.prototype)
}

// 使用 CustomToString 装饰器
@CustomToString
class Person {
  constructor(public name: string, public age: number) {}
  speak() {
    console.log('你好呀!')
  }
}

/* 测试代码如下 */
let p1 = new Person('张三', 18)
// 输出:{"name":"张三","age":18}
console.log(p1.toString())

// 禁止随意操作其原型对象
interface Person {
  a: any
}
Person.prototype.a = 100 // 此行运行时会报错:Cannot add property a, object is not extensible
console.log(p1.a)

3. 关于返回值

  • 类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类。

  • 若类装饰器无返回值:或返回undefined,那被装饰的类不会被替换。

function demo(target: Function) {
  // 装饰器有返回值时,该返回值会替换掉被装饰的类
  return class {
    test() {
      console.log(200)
    }
  }
}

@demo
class Person {
  test() {
    console.log(100)
  }
}

console.log(Person) // [class (anonymous)]

const p1 = new Person()
p1.test() // 打印 200

4. 关于构造类型

在 TypeScript 中,Function类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。但并非Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么 TypeScript 中如何声明一个构造类型呢?有以下两种方式:

// 定义Constructor类型,其含义是构造类型
type Constructor = new (...args: any[]) => {}
 // new     表示:该类型是可以用new操作符调用。
 // ...args 表示:构造器可以接受【任意数量】的参数。
 // any[]   表示:构造器可以接受【任意类型】的参数。
 // {}      表示:返回类型是对象(非null、非undefined的对象)。

function test(fn: Constructor) {}
class Person {}
test(Person)

// 定义Constructor类型,且包含一个静态属性 country
type Constructor = {
  new (...args: any[]): {} // 构造签名
  country: string // country静态属性
}

function test(fn: Constructor) {}
class Person {
  static country = 'cn'
}
test(Person)

5. 替换被装饰的类

对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。比如说:下面设计了一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,又添加了一个方法用于读取创建时间。

// User接口
interface User {
  getTime(): Date
  log(): void
}

// 自定义类型Class
type Constructor = new (...args: any[]) => {}

// 创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target: T) {
  return class extends target {
    createdTime: Date
    constructor(...args: any[]) {
      super(...args)
      this.createdTime = new Date() // 记录对象创建时间
    }
    getTime() {
      return `该对象创建时间为:${this.createdTime}`
    }
  }
}

@LogTime
class User {
  constructor(public name: string, public age: number) {}
  speak() {
    console.log(`${this.name}说:你好啊!`)
  }
}

const user1 = new User('张三', 13)
user1.speak() // 张三说:你好啊!
console.log(user1.getTime()) // 该对象创建时间为:Sat Sep 28 2024 10:13:46 GMT+0800 (中国标准时间)

装饰器工厂

1. 什么是装饰器工厂

一个函数返回装饰器函数就是装饰器工厂:

// 定义一个装饰器工厂 LogInfo,它接受一个参数 n,返回一个类装饰器
function LogInfo(n: number) { // 通过装饰器工厂,现在可以给装饰器传参了
  // 装饰器函数,target 是被装饰的类
  return function (target: Function) {
    target.prototype.introduce = function () {
      for (let i = 0; i < n; i++) {
        console.log(`我的名字:${this.name},我的年龄:${this.age}`)
      }
    }
  }
}

interface Person {
  introduce(): void
}

@LogInfo(3)
class Person {
  constructor(public name: string, public age: number) {}
  speak() {
    console.log('你好呀!')
  }
}

let p1 = new Person('张三', 18)
p1.introduce()

2. 装饰器组合及其执行顺序

装饰器可以组合使用,执行顺序为先由上到下的执行所有装饰器工厂,依次获取到装饰器,然后再由下到上执行所有的装饰器。看代码执行:

//装饰器
function test1(target: Function) {
  console.log('test1')
}
//装饰器工厂
function test2() {
  console.log('test2工厂')
  return function (target: Function) {
    console.log('test2')
  }
}
//装饰器工厂
function test3() {
  console.log('test3工厂')
  return function (target: Function) {
    console.log('test3')
  }
}
//装饰器
function test4(target: Function) {
  console.log('test4')
}

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

/*
  控制台打印:
    test2工厂
    test3工厂
    test4
    test3
    test2
    test1
*/

属性装饰器

1. 基本语法

function Demo(target: object, propertyKey: string) {
  // target: 对于静态属性来说值是类,对于实例属性来说值是类的原型对象
  // propertyKey: 属性名
  console.log(target, propertyKey)
}

class Person {
  @Demo name: string // 实例属性,类的原型对象
  @Demo age: number // 实例属性,类的原型对象
  @Demo static school: string // 静态属性,类

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

const p1 = new Person('张三', 18)

// 打印
// {} name
// {} age
// [class Person] school

2. 关于属性遮蔽

如下代码中:当构造器中的this.age = age试图在实例上赋值时,但没有找到,使用实际上是调用了原型上age属性的set方法。

class Person {
  name: string
  age: number // 注意,这只是类型声明,没有age
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

let value = 99
// 使用defineProperty给Person原型添加age属性,并配置对应的get与set
Object.defineProperty(Person.prototype, 'age', {
  get() {
    return value
  },
  set(val) {
    value = val
  },
})

const p1 = new Person('张三', 18)
console.log(p1.age) //18
console.log(Person.prototype.age) //18

3. 应用举例

下面代码定义了一个State属性装饰器,来监视属性的修改。

// 声明一个装饰器函数 State,用于捕获数据的修改
function State(target: object, propertyKey: string) {
  // 存储属性的内部值
  let key: string = `__${propertyKey}`

  // 使用 Object.defineProperty 替换类的原始属性
  // 重新定义属性,使其使用自定义的 getter 和 setter
  Object.defineProperty(target, propertyKey, {
    get() {
      // 这里取到this身上就可以不同对象用自己的key了,不会出现共用,只不过会在对象上追加一个缓存值``__${propertyKey}``
      return this[key] // 这里不能用`.`,因为key是字符串
    },
    set(newVal: string) {
      console.log(`${propertyKey}的最新值为:${newVal}`)
      this[key] = newVal
    },

    enumerable: true, // 可枚举
    configurable: true, // 可配置
  })
}

class Person {
  name: string
  // 使用State装饰器
  @State age: number
  school = 'peking university'
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

const p1 = new Person('张三', 18)
const p2 = new Person('李四', 30)

p1.age = 80
p2.age = 90

console.log('------------------')
console.log(p1.age) //80
console.log(p2.age) //90

方法装饰器

1. 基本语法

function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
  //  target: 对于静态方法来说值是【类】,对于实例方法来说值是【原型对象】。
  //  propertyKey:方法的名称。
  //  descriptor: 方法的描述对象,其中【value】属性是被装饰的【方法】(不仅仅是方法名),还有可枚举、可配置、可写属性。

  console.log(target)
  console.log(propertyKey)
  console.log(descriptor)
}

class Person {
  constructor(public name: string, public age: number) {}
  // Demo装饰实例方法
  @Demo speak() {
    console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
  }
  // Demo装饰静态方法
  @Demo static isAdult(age: number) {
    return age >= 18
  }
}

Person.isAdult(18)
const p1 = new Person('张三', 18)
p1.speak()

2. 应用举例

下面代码中Logger方法装饰器在方法前后给其增加了一些额外的逻辑,Validate装饰器用于验证数据

// 打印日志的装饰器
function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
  // 保存原始方法
  const original = descriptor.value
  // 替换原始方法
  descriptor.value = function (...args: any[]) {
    console.log(`${propertyKey}开始执行......`)
    const result = original.call(this, ...args)
    console.log(`${propertyKey}执行完毕......`)
    return result
  }
}

// 验证参数的装饰器工厂
function Validate(maxValue: number) {
  return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始方法
    const original = descriptor.value
    // 替换原始方法
    descriptor.value = function (...args: any[]) {
      // 自定义的验证逻辑
      if (args[0] > maxValue) {
        throw new Error('年龄非法!')
      }
      // 如果所有参数都符合要求,则调用原始方法
      return original.apply(this, args)
    }
  }
}

class Person {
  constructor(public name: string, public age: number) {}
  @Logger speak() {
    console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
  }
  @Validate(120)
  static isAdult(age: number) {
    return age >= 18
  }
}

const p1 = new Person('张三', 18)
p1.speak()
console.log(Person.isAdult(100))

访问器装饰器

1. 基本语法

function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
  // target:
  //   1. 对于实例访问器来说值是【所属类的原型对象】
  //   2. 对于静态访问器来说值是【所属类】
  // propertyKey:访问器的名称
  // descriptor: 描述对象,一样的可以通过value取得方法

  console.log(target)
  console.log(propertyKey)
  console.log(descriptor)
}

class Person {
  @Demo
  get address() {
    return '中国深圳'
  }
  @Demo
  static get country() {
    return '中国'
  }
}

2. 应用举例

以下代码对Weather类的temp属性的set访问器进行限制,设置的最低温度-50,最高温度50

// 限制访问器值设置范围的装饰器
function RangeValidate(min: number, max: number) {
  return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始的 setter 方法,以便在后续调用中使用
    const originalSetter = descriptor.set

    // 重写 setter 方法,加入范围验证逻辑
    descriptor.set = function (value: number) {
      // 检查设置的值是否在指定的最小值和最大值之间
      if (value < min || value > max) {
        // 如果值不在范围内,抛出错误
        throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`)
      }

      // 如果值在范围内,且原始 setter 方法存在,则调用原始 setter 方法
      if (originalSetter) {
        originalSetter.call(this, value)
      }
    }
  }
}

class Weather {
  private _temp: number
  constructor(_temp: number) {
    this._temp = _temp
  }
  // 设置温度范围在 -50 到 50 之间
  @RangeValidate(-50, 50)
  set temp(value) {
    this._temp = value
  }
  get temp() {
    return this._temp
  }
}

const w1 = new Weather(25)
console.log(w1)
w1.temp = 67
console.log(w1)

3. 访问器装饰器和方法装饰器的区别

  • 描述器descriptor中的key不同:方法装饰器的描述器的keyvaluewritableenumerableconfigurable,访问器装饰器的描述器keygetsetenumerableconfigurable
  • 返回值类型不同:访问器装饰器返回一个TypedPropertyDescriptor<any> 类型的对象,而方法装饰器返回一个 TypedPropertyDescriptor<T> 类型的对象,其中 T` 是方法的返回类型

参数装饰器

1. 基本语法

function Demo(target: object, propertyKey: string, parameterIndex: number) {
  // target:
  //    1.如果修饰的是【实例方法】的参数,target 是类的【原型对象】
  //    2.如果修饰的是【静态方法】的参数,target 是【类】
  // propertyKey:参数所在的方法的名称
  // parameterIndex: 参数在函数参数列表中的索引,从 0 开始
  console.log(target) // {}
  console.log(propertyKey) // speak
  console.log(parameterIndex) // 0
}

// 类定义
class Person {
  constructor(public name: string) {}
  speak(@Demo message1: any, mesage2: any) {
    console.log(`${this.name}想对说:${message1},${mesage2}`)
  }
}

2. 应用举例

下面代码定义了一个方法装饰器Validate,同时搭配参数装饰器NotNumber,来对speak方法的参数类型进行限制

// 参数装饰器定义
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
  // 初始化或获取当前方法的参数索引列表
  let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || []
  // 将当前参数索引添加到列表中
  notNumberArr.push(parameterIndex)
  // 将列表存储回目标对象
  target[`__notNumber_${propertyKey}`] = notNumberArr
}

// 方法装饰器定义
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  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, message2: any) {
    console.log(`${this.name}想对说:${message1},${message2}`)
  }
}

// 使用
const s1 = new Student('张三')
s1.speak('100', 200) // 不抛出异常
s1.speak('100', 200) // 抛出异常

一个 TypedPropertyDescriptor<T> 类型的对象,其中 T` 是方法的返回类型

参数装饰器

1. 基本语法

function Demo(target: object, propertyKey: string, parameterIndex: number) {
  // target:
  //    1.如果修饰的是【实例方法】的参数,target 是类的【原型对象】
  //    2.如果修饰的是【静态方法】的参数,target 是【类】
  // propertyKey:参数所在的方法的名称
  // parameterIndex: 参数在函数参数列表中的索引,从 0 开始
  console.log(target) // {}
  console.log(propertyKey) // speak
  console.log(parameterIndex) // 0
}

// 类定义
class Person {
  constructor(public name: string) {}
  speak(@Demo message1: any, mesage2: any) {
    console.log(`${this.name}想对说:${message1},${mesage2}`)
  }
}

2. 应用举例

下面代码定义了一个方法装饰器Validate,同时搭配参数装饰器NotNumber,来对speak方法的参数类型进行限制

// 参数装饰器定义
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
  // 初始化或获取当前方法的参数索引列表
  let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || []
  // 将当前参数索引添加到列表中
  notNumberArr.push(parameterIndex)
  // 将列表存储回目标对象
  target[`__notNumber_${propertyKey}`] = notNumberArr
}

// 方法装饰器定义
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  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, message2: any) {
    console.log(`${this.name}想对说:${message1},${message2}`)
  }
}

// 使用
const s1 = new Student('张三')
s1.speak('100', 200) // 不抛出异常
s1.speak('100', 200) // 抛出异常