死磕 TypeScript 高级技巧之全网终极总结

本文深入讲解TypeScript的各种特性和用法,包括类型、运算符、泛型、类、接口及装饰器等内容,帮助读者掌握这门强大的静态类型语言。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 一 类型

  • 二 运算符

  • 三 操作符

  • 四 范型

  • 五 范型工具

  • 六 类

  • 七 接口

  • 八 装饰器

一 类型

unknown

unknown 说的是不可预先定义的类型,在多数情况下,它用于代替 any 的功可以同时保留静态检查的可以力。

const num: number = 10;
(num as unknown as string).split('');   // 注意,这里和any一样完全用于通过静态检查

此时 unknown 的作用就和 any 高度类似了,你用于把它转变成任何类型,区别是,在静态编译的时候,unknown 不可以调用任何方法,而 any 用于。

const foo: unknown = 'string';
foo.substr(1);    // Error: 静态检查不通过报错
const bar: any = 10;
any.substr(1);  // Pass: any类型相当于放弃了静态检查

unknown 的一个使用场景是,避免使用 any 作为函数的参数类型而导致的静态类型检查 bug:

function test(input: unknown): number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: 这个代码块中,类型守卫已经将input识别为array类型
  }
  return input.length;      // Error: 这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险
}

Enum类型

使用枚举我们用于很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及用于用它来描述一些状态信息,比如错误码等

// 字符串枚举 每个都需要声明
enum Color {
  RED = "红色",
  PINK = "粉色",
  BLUE = "蓝色",
}
const pink: Color = Color.PINK;
console.log(pink); // 粉色

enum Color {
  RED = 10,
  PINK,
  BLUE,
}
const pink: Color = Color.PINK;
console.log(pink); // 11

void

在 TS 中,void 与 undefined 功可以很类似,用于在逻辑上避免不经意使用了空指针导致的错误。

function foo() {}   // 这个空函数没有返回任何值,返回类型缺省为void
const a = foo(); // 此时a的类型定义为void,你也不可以调用a的任何属性方法

void 和 undefined 类型最大的区别是, undefined 是 void 的子集,当你对函数返回值并不在意时,使用 void 而不是 undefined。

// Parent.tsx
function Parent(): JSX.Element {
  const getValue = (): number => { return 2 };    /* 这里函数返回的是number类型 */
  // const getValue = (): string => { return 'str' }; /* 这里函数返回的string类型,同样用于传给子属性 */
  return <Child getValue={getValue} />
}
// Child.tsx
type Props = {
  getValue: () => void;  // 这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都用于
}
function Child({ getValue }: Props) => <div>{getValue()}</div>

元组类型(tuple)

在 TypeScript 的基础类型中,元组( Tuple )表示一个已知数量和类型的数组 其实用于理解为他是一种特殊的数组

const flag: [string, number] = ["hello", 1];

never

never 是说没法正常结束返回的类型,一个必定会报错或死循环的函数会返回这样的类型。

function foo(): never { throw new Error('error message') }  // throw error 返回值是never
function foo(): never { while(true){} }  // 这个死循环的也会无法正常退出
function foo(): never { let count = 1; while(count){ count ++; } }  // Error: 这个无法将返回值定义为never,因为无法在静态编译阶段直接识别出

还有就是永远没有相交的类型:

type human = 'boy' & 'girl' // 这两个单独的字符串类型并不可可以相交,故human为never类型

不过任何类型联合上 never 类型,还是原来的类型:

type language = 'ts' | never   // language的类型还是'ts'类型

关于 never 有如下特性:

在一个函数中调用了返回 never 的函数后,之后的代码都会变成deadcode

function test() {
  foo();    // 这里的foo指上面返回never的函数
  console.log(111);  // Error: 编译器报错,此行代码永远不会执行到
}

无法把其他类型赋给 never:

let n: never;
let o: any = {};
n = o;  // Error: 不可以把一个非never类型赋值给never类型,包括any

关于 never 的这个特性有一些很 hack 的用法和讨论,比如这个知乎下的尤雨溪的回答。

二 运算符

交叉类型运算符

交叉类型是将多个类型合并为一个类型。通过 & 运算符用于将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

type Flag1 = { x: number };
type Flag2 = Flag1 & { y: string };

let flag3: Flag2 = {
  x: 1,
  y: "hello",
  henb,
};

空值合并运算符 ??

??与||的作用是基本相似,不同点在于 ??在左侧表达式结果为 null 或者 undefined 时,才会返回右侧表达式 。比如我们书写了let b = a ?? 10,生成的代码如下:

let b = a !== null && a !== void 0 ? a : 10;

而 || 表达式,大家知道的,则对 false、''、NaN、0 等逻辑空值也会生效,不适于我们做对参数的合并。

数字分隔符_

let num:number = 1_2_345.6_78_9

_用于用来对长数字做任意的分隔,主要设计是为了便于数字的阅读,编译出来的代码是没有下划线的,请放心食用。

非空断言运算符 !

这个运算符用于用在变量名或者函数名之后,用来强调对应的元素是非 null|undefined 的

function onClick(callback?: () => void) {
  callback!();  // 参数是可选入参,加了这个感叹号!之后,TS编译不报错
}

你用于查看编译后的 ES5 代码,居然没有做任何防空判断。

function onClick(callback) {
  callback();
}

这个符号的场景,特别适用于我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 Ref。

function Demo(): JSX.Elememt {
  const divRef = useRef<HTMLDivElement>();
  useEffect(() => {
    divRef.current!.scrollIntoView();  // 当组件Mount后才会触发useEffect,故current一定是有值的
  }, []);
  return <div ref={divRef}>Demo</div>
}

可选链运算符 ?.

相比上面!作用于编译阶段的非空判断,?.这个是开发者最需要的运行时(当然编译时也有效)的非空判断。

obj?.prop    obj?.[index]    func?.(args)

?.用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,用于减少我们大量的&&运算。比如我们写出a?.b时,编译器会自动生成如下代码

a === null || a === void 0 ? void 0 : a.b;

这里涉及到一个小知识点:undefined这个值在非严格模式下会被重新赋值,使用void 0必定返回真正的 undefined。

联合类型运算符

联合类型(Union Types)表示取值用于为多种类型中的一种 未赋值时联合类型上只可以访问两个类型共有的属性和方法

let name: string | number;
console.log(name.toString());
name = 1;
console.log(name.toFixed(2));
name = "hello";
console.log(name.length);

三、操作符

遍历属性 in

in 只可以用在类型的定义中,用于对枚举类型进行遍历,如下:

// 这个类型用于将任何类型的键值转化成number类型
type TypeToNumber<T> = {
  [key in keyof T]: number
}

keyof返回泛型 T 的所有键枚举类型,key是自定义的任何变量名,中间用in链接,外围用[]包裹起来(这个是固定搭配),冒号右侧number将所有的key定义为number类型。于是用于这样使用了:

const obj: TypeToNumber<Person> = { name: 10, age: 10 }

总结起来 in 的语法格式如下:

[ 自定义变量名 in 枚举类型 ]: 类型

键值获取 keyof

keyof 获取一个类型所有键值,返回一个联合类型,如下:

type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  // PersonKey得到的类型为 'name' | 'age'

keyof 的一个典型用途是限制访问对象的 key 合法化,因为 any 做索引是不被接受的。

function getValue (p: Person, k: keyof Person) {
  return p[k];  // 如果k不如此定义,则无法以p[k]的代码格式通过编译
}

总结起来 keyof 的语法格式如下

类型 = keyof 类型

实例类型获取 typeof

typeof 是获取一个对象/实例的类型,如下:

const me: Person = { name: 'gzx', age: 16 };
type P = typeof me;  // { name: string, age: number | undefined }
const you: typeof me = { name: 'mabaoguo', age: 69 }  // 用于通过编译

typeof 只可以用在具体的对象上,这与 js 中的 typeof 是一致的,并且它会根据左侧值自动决定应该执行哪种行为。

const typestr = typeof me;   // typestr的值为"object"
typeof 用于和 keyof 一起使用(因为 typeof 是返回一个类型嘛),如下:
type PersonKey = keyof typeof me;   // 'name' | 'age'

总结起来 typeof 的语法格式如下:

类型 = typeof 实例对象

四、泛型

泛型在 TS 中可以说是一个非常重要的属性,它承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的元编程。在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。泛型可以说是 TS 类型工具的精髓所在,也是整个 TS 最难学习的部分,这里专门分两章总结一下。

对于刚接触 TypeScript 泛型的读者来说,首次看到语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。

9e3a78c88b954a5dc906b1d2225ec16d.png

参考上面的图片,当我们调用 identity(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;

  • V(Value):表示对象中的值类型;

  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(68, "Semlinker"));
5675a74f60c81baf0cc6b53b6c89e3b7.png

基本使用

泛型可以用在普通类型定义,类定义、函数定义上,如下:

// 普通类型定义
type Duck<T> = { name: string, type: T }
// 普通类型使用
const duck: Duck<number> = { name: 'ww', type: 20 }

// 类定义
class Bird<T> {
  private type: T;
  constructor(type: T) { this.type = type; }
}
// 类使用
const bird: Bird<number> = new Bird<number>(20); // 或简写 const bird = new Bird(20)

// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {
  return [value[1], value[0]];
}
// 函数使用
swipe<Bird<number>, Duck<number>>([bird, duck])  // 或简写 swipe([bird, duck])

注意,如果对一个类型名定义了泛型,那么使用此类型名的时候一定要把泛型类型也写上去。

而对于变量而言,它的类型如果可以在调用时推断出来的话,就可以省略泛型书写。

泛型的语法格式简单总结如下:

类型名<泛型列表> 具体类型定义

泛型推导和默认值

上面提到了,我们可以简化对泛型类型定义的书写,因为TS会自动根据变量定义时的类型推导出变量类型,这一般是发生在函数调用的场合的。

type Duck<T> = { name: string, type: T }

function adopt<T>(duck: Duck<T>) { return duck };

const Duck = { name: 'ww', type: 'hsq' };  // 这里按照Duck类型的定义一个type为string的对象
adopt(Duck);  // Pass: 函数会根据入参类型推断出type为string

若不适用函数泛型推导,我们若需要定义变量类型则必须指定泛型类型。

const Duck: Duck<string> = { name: 'ww', type: 'hsq' }  // 不可省略<string>这部分

如果我们想不指定,可以使用泛型默认值的方案。

type Duck<T = any> = { name: string, type: T }
const Duck: Duck = { name: 'ww', type: 'hsq' }
duck.type = 123;    // 不过这样type类型就是any了,无法自动推导出来,失去了泛型的意义

泛型默认值的语法格式简单总结如下:

泛型名 = 默认类型

泛型约束

有的时候,我们可以不用关注泛型具体的类型,如:

function fill<T>(length: number, value: T): T[] {
  return new Array(length).fill(value);
}

这个函数接受一个长度参数和默认值,结果就是生成使用默认值填充好对应个数的数组。我们不用对传入的参数做判断,直接填充就行了,但是有时候,我们需要限定类型,这时候使用extends关键字即可。

function sum<T extends number>(value: T[]): number {
  let count = 0;
  value.forEach(v => count += v);
  return count;
}

这样你就可以以sum([1,2,3])这种方式调用求和函数,而像sum(['1', '2'])这种是无法通过编译的。

泛型约束也可以用在多个泛型参数的情况。

function pick<T, U extends keyof T>(){};

这里的意思是限制了 U 一定是 T 的 key 类型中的子集,这种用法常常出现在一些泛型工具库中。

extends 的语法格式简单总结如下,注意下面的类型既可以是一般意义上的类型也可以是泛型。

泛型名 extends 类型

泛型条件

在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。上面提到 extends,其实也可以当做一个三元运算符,如下:

T extends U? X: Y

条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一, 这里便不限制 T 一定要是 U 的子类型,如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。

注意,生成的结果是分配式的。

举个例子,如果我们把 X 换成 T,如此形式:T extends U? T: never。

此时返回的 T,是满足原来的 T 中包含 U 的部分,可以理解为 T 和 U 的交集。

所以,extends 的语法格式可以扩展为

泛型名A extends 类型B ? 类型C: 类型D

泛型推断 infer

infer 的中文是“推断”的意思,一般是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过你得先预定义好整体的结构。举个例子

type Fo<T> = T extends {t: infer Test} ? Test: string

首选看 extends 后面的内容,{t: infer Test}可以看成是一个包含t属性的类型定义,这个t属性的 value 类型通过infer进行推断后会赋值给Test类型,如果泛型实际参数符合{t: infer Test}的定义那么返回的就是Test类型,否则默认给缺省的string类型。

举个例子加深下理解:

type One = Fo<number>  // string,因为number不是一个包含t的对象类型
type Two = Fo<{t: boolean}>  // boolean,因为泛型参数匹配上了,使用了infer对应的type
type Three = Fo<{a: number, t: () => void}> // () => void,泛型定义是参数的子集,同样适配

infer用来对满足的泛型类型进行子类型的抽取,有很多高级的泛型工具也巧妙的使用了这个方法。

interface Dictionary<T = any> {
  [key: string]: T;
}
 
type StrDict = Dictionary<string>

type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string

在上面示例中,当类型 T 满足 T extends Dictionary 约束时,我们会使用 infer 关键字声明了一个类型变量 V,并返回该类型,否则返回 never 类型。除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:

async function stringPromise() {
  return "Hello, Semlinker!";
}

interface Person {
  name: string;
  age: number;
}

async function personPromise() {
  return { name: "Semlinker", age: 30 } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person

五、泛型工具

Pick<T, K>

此工具的作用是将 T 类型中的 K 键列表提取出来,生成新的子键值对类型。

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

我们用上面的Animal定义,看一下 Pick 如何使用。

const bird: Pick<Animal, "name" | "age"> = { name: 'bird', age: 1 }

Record<K, T>

此工具的作用是将 K 中所有属性值转化为 T 类型,我们常用它来申明一个普通 object 对象。

type Record<K extends keyof any,T> = {
  [key in K]: T
}

这里特别说明一下,keyof any对应的类型为number | string | symbol,也就是用于做对象键(专业说法叫索引 index)的类型集合。举个例子:

const obj: Record<string, string> = { 'name': 'zhangsan', 'tag': '打工人' }

Exclude<T, U>

此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。

type Exclude<T, U> = T extends U ? never : T

注意这里的 extends 返回的 T 是原来的 T 中和 U 无交集的属性,而任何属性联合 never 都是自身,具体可在上文查阅。举个例子

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;   // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract<T,U>

从T中抽出可分配给U的属性构成新的类型。与Exclude相反

type T0 = Extract<'a' | 'b' | 'c', 'a'>; 

// = 

type T0 = 'a'

Partial

此工具的作用就是将泛型中全部属性变为可选的。

type Partial<T> = {
 [P in keyof T]?: T[P]
}

举个例子,这个类型定义在下面也会用到。

type Animal = {
  name: string,
  category: string,
  age: number,
  eat: () => number
}

使用 Partial 包裹一下。

type PartOfAnimal = Partial<Animal>;
const ww: PartOfAnimal = { name: 'ww' }; // 属性全部可选后,用于只赋值部分属性了

Parameters

返回类型为T的函数的参数类型所组成的数组

type T0 = Parameters<() => string>;  // []

type T1 = Parameters<(s: string) => void>;  // [string]

Omit<T, K>

此工具可认为是适用于键值对对象的 Exclude,它会去除类型 T 中包含 K 的键值对。

type Omit = Pick<T, Exclude<keyof T, K>>

在定义中,第一步先从 T 的 key 中去掉与 K 重叠的 key,接着使用 Pick 把 T 类型和剩余的 key 组合起来即可。还是用上面的 Animal 举个例子:

const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion', eat: () => { console.log('eat') } }

用于发现,Omit 与 Pick 得到的结果完全相反,一个是取非结果,一个取交结果。

InstanceType

返回构造函数类型T的实例类型

class C {
  x = 0;
  y = 0;
}

type T0 = InstanceType<typeof C>;  // C

ReturnType

此工具就是获取 T 类型(函数)对应的返回值类型:

type ReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : any;

看源码其实有点多,其实用于稍微简化成下面的样子:

type ReturnType<T extends func> = T extends () => infer R ? R: any;

使用 infer 推断返回值类型,进而返回此类型,如果你理解了 infer 的含义,上面的源码就比较好理解了。

举个例子:

function foo(x: string | number): string | number { /*..*/ }
type FooType = ReturnType<foo>;  // string | number

Required

此工具用于将类型 T 中所有的属性变为必选项。

type Required<T> = {
  [P in keyof T]-?: T[P]
}

这里有一个很有意思的语法-?,你用于理解为就是 TS 中把?可选属性减去的意思。

除了这些以外,还有很多的内置的类型工具,用于参考TypeScript Handbook获得更详细的信息,同时 Github 上也有很多第三方类型辅助工具,如utility-types等。

六 类

readonly 只读属性

readonly 修饰的变量只可以在构造函数中初始化 TypeScript 的类型系统同样也允许将 interface、type、 class 上的属性标识为 readonly readonly 实际上只是在编译阶段进行代码检查。

class Animal {
  public readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
  changeName(name: string) {
    this.name = name; //这个ts是报错的
  }
}
let a = new Animal("hello");

抽象类和抽象方法

抽象类,无法被实例化,只可以被继承并且无法创建抽象类的实例

子类用于对抽象类进行不同的实现

抽象方法只可以出现在抽象类中并且抽象方法不可以在抽象类中被具体实现,只可以在抽象类的子类中实现(必须要实现)

使用场景:

我们一般用抽象类和抽象方法抽离出事物的共性 以后所有继承的子类必须按照规范去实现自己的具体逻辑 这样用于增加代码的可维护性和复用性

使用 abstract 关键字来定义抽象类和抽象方法

abstract class Animal {
  name!: string;
  abstract speak(): void;
}
class Cat extends Animal {
  speak() {
    console.log("喵喵喵");
  }
}
let animal = new Animal(); //直接报错 无法创建抽象类的实例
let cat = new Cat();
cat.speak();

思考 1:重写(override)和重载(overload)的区别

  • 重写是指子类重写继承自父类中的方法

  • 重载是指为同一个函数提供多个类型定义

class Animal {
  speak(word: string): string {
    return "动物:" + word;
  }
}
class Cat extends Animal {
  speak(word: string): string {
    return "猫:" + word;
  }
}
let cat = new Cat();
console.log(cat.speak("hello"));
// 上面是重写
//--------------------------------------------
// 下面是重载
function double(val: number): number;
function double(val: string): string;
function double(val: any): any {
  if (typeof val == "number") {
    return val * 2;
  }
  return val + val;
}

let r = double(1);
console.log(r);

思考 2:什么是多态

在父类中定义一个方法,在子类中有多个实现,在程序运行的时候,根据不同的对象执行不同的操作,实现运行时的绑定。

abstract class Animal {
  // 声明抽象的方法,让子类去实现
  abstract sleep(): void;
}
class Dog extends Animal {
  sleep() {
    console.log("dog sleep");
  }
}
let dog = new Dog();
class Cat extends Animal {
  sleep() {
    console.log("cat sleep");
  }
}
let cat = new Cat();
let animals: Animal[] = [dog, cat];
animals.forEach((i) => {
  i.sleep();
});

七 接口

接口既用于在面向对象编程中表示为行为的抽象,也用于用来描述对象的形状

我们用 interface 关键字来定义接口 在接口中用于用分号或者逗号分割每一项,也用于什么都不加

描述 对象的形状

//接口用于用来描述`对象的形状`
//接口用于用来描述`对象的形状`
interface Speakable {
  speak(): void;
  readonly lng: string; //readonly表示只读属性 后续不用于更改
  name?: string; //?表示可选属性
}

let speakman: Speakable = {
  //   speak() {}, //少属性会报错
  name: "hello",
  lng: "en",
  age: 111, //多属性也会报错
};

行为的抽象

接口用于把一些类中共有的属性和方法抽象出来,用于用来约束实现此接口的类

一个类用于实现多个接口,一个接口也用于被多个类实现

我们用 implements关键字来代表 实现

//接口用于在面向对象编程中表示为行为的抽象
interface Speakable {
  speak(): void;
}
interface Eatable {
  eat(): void;
}
//一个类用于实现多个接口
class Person implements Speakable, Eatable {
  speak() {
    console.log("Person说话");
  }
  //   eat() {} //需要实现的接口包含eat方法 不实现会报错
}

定义任意属性

如果我们在定义接口的时候无法预先知道有哪些属性的时候,用于使用 [propName:string]:any,propName 名字是任意的

interface Person {
  id: number;
  name: string;
  [propName: string]: any;
}

let p1 = {
  id: 1,
  name: "hello",
  age: 10,
};

这个接口表示 必须要有 id 和 name 这两个字段 然后还用于新加其余的未知字段

接口的继承

我们除了类用于继承 接口也用于继承 同样的使用 extends关键字

interface Speakable {
  speak(): void;
}
interface SpeakChinese extends Speakable {
  speakChinese(): void;
}
class Person implements SpeakChinese {
  speak() {
    console.log("Person");
  }
  speakChinese() {
    console.log("speakChinese");
  }
}

八 装饰器

装饰器是一种特殊类型的声明,它可以够被附加到类声明、方法、属性或参数上,用于修改类的行为 常见的装饰器有类装饰器、属性装饰器、方法装饰器和参数装饰器 装饰器的写法分为普通装饰器和装饰器工厂 使用@装饰器的写法需要把 tsconfig.json 的 experimentalDecorators 字段设置为 true

类装饰器

类装饰器在类声明之前声明,用来监视、修改或替换类定义

namespace a {
  //当装饰器作为修饰类的时候,会把构造器传递进去
  function addNameEat(constructor: Function) {
    constructor.prototype.name = "hello";
    constructor.prototype.eat = function () {
      console.log("eat");
    };
  }
  @addNameEat
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}

namespace b {
  //还用于使用装饰器工厂 这样用于传递额外参数
  function addNameEatFactory(name: string) {
    return function (constructor: Function) {
      constructor.prototype.name = name;
      constructor.prototype.eat = function () {
        console.log("eat");
      };
    };
  }
  @addNameEatFactory("hello")
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}

namespace c {
  //还用于替换类,不过替换的类要与原类结构相同
  function enhancer(constructor: Function) {
    return class {
      name: string = "jiagou";
      eat() {
        console.log("吃饭饭");
      }
    };
  }
  @enhancer
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log(p.name);
  p.eat();
}

属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入 2 个参数 第一个参数对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 第二个参数是属性的名称

//修饰实例属性
function upperCase(target: any, propertyKey: string) {
  let value = target[propertyKey];
  const getter = function () {
    return value;
  };
  // 用来替换的setter
  const setter = function (newVal: string) {
    value = newVal.toUpperCase();
  };
  // 替换属性,先删除原先的属性,再重新定义属性
  if (delete target[propertyKey]) {
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  }
}

class Person {
  @upperCase
  name!: string;
}
let p: Person = new Person();
p.name = "world";
console.log(p.name);

方法装饰器

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

propertyKey: string | symbol - 方法名

descriptor: TypePropertyDescript - 属性描述符

//修饰实例方法
function noEnumerable(
  target: any,
  property: string,
  descriptor: PropertyDescriptor
) {
  console.log("target.getName", target.getName);
  console.log("target.getAge", target.getAge);
  descriptor.enumerable = false;
}
//重写方法
function toNumber(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  let oldMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    args = args.map((item) => parseFloat(item));
    return oldMethod.apply(this, args);
  };
}
class Person {
  name: string = "hello";
  public static age: number = 10;
  constructor() {}
  @noEnumerable
  getName() {
    console.log(this.name);
  }
  @toNumber
  sum(...args: any[]) {
    return args.reduce((accu: number, item: number) => accu + item, 0);
  }
}
let p: Person = new Person();
for (let attr in p) {
  console.log("attr=", attr);
}
p.getName();
console.log(p.sum("1", "2", "3"));

参数装饰器

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

target: Object - 被装饰的类 propertyKey: string | symbol - 方法名 parameterIndex: number - 方法中参数的索引值

function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
 been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase;
  }
}

以上代码成功运行后,控制台会输出以下结果:"The parameter in position 0 at Greeter has been decorated"

装饰器执行顺序

有多个参数装饰器时:从最后一个参数依次向前执行

方法和方法参数中参数装饰器先执行。方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行

类装饰器总是最后执行

function Class1Decorator() {
  return function (target: any) {
    console.log("类1装饰器");
  };
}
function Class2Decorator() {
  return function (target: any) {
    console.log("类2装饰器");
  };
}
function MethodDecorator() {
  return function (
    target: any,
    methodName: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("方法装饰器");
  };
}
function Param1Decorator() {
  return function (target: any, methodName: string, paramIndex: number) {
    console.log("参数1装饰器");
  };
}
function Param2Decorator() {
  return function (target: any, methodName: string, paramIndex: number) {
    console.log("参数2装饰器");
  };
}
function PropertyDecorator(name: string) {
  return function (target: any, propertyName: string) {
    console.log(name + "属性装饰器");
  };
}

@Class1Decorator()
@Class2Decorator()
class Person {
  @PropertyDecorator("name")
  name: string = "hello";
  @PropertyDecorator("age")
  age: number = 10;
  @MethodDecorator()
  greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) {}
}

/**
name属性装饰器
age属性装饰器
参数2装饰器
参数1装饰器
方法装饰器
类2装饰器
类1装饰器
 */

参考文章:

  • https://juejin.cn/post/7031787942691471396#heading-64

  • https://juejin.cn/post/6926794697553739784#heading-19

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及下方前端精选资料 添加 下方小助手二维码就可以进群。让我们一起学习进步.

f737ce730c025ff349fa90ee795728ca.png

3507775afab01284b2141382b96c9983.png


推荐阅读

(点击标题可跳转阅读)

[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-阿里,网易,滴滴,头条等20家面试真题

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-Javascript条件逻辑设计重构

[自我提升]-送给React开发者十九条性能优化建议

[自我提升]-页面可视化工具的前世今生

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历

觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

1a1eea618b675927df5ec391e7ca09b1.png   

点个赞👍🏻,顺便点个 在看 支持下我吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值