还有我不知道的TypeScript知识?-细节篇(卷王必看)

上篇介绍了typescript的基本用法以及一些基本的知识点,这篇文章将介绍更深入细节一点的typescript知识

函数
局部类型

函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。

function hello(txt:string) {
  type message = string;
  let newTxt:message = 'hello ' + txt;
  return newTxt;
}
const newTxt:message = hello('world'); // 报错

上面示例中,类型message是在函数hello()内部定义的,只能在函数内部使用。在函数外部使用,就会报错。

高阶函数

一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)

(a: number) => (b: number) => a * b
函数重载

有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)

reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]
//参数可以是字符串,也可以是数组。

该函数内部有处理字符串和数组的两套逻辑,根据参数类型的不同,分别执行对应的逻辑。这就叫“函数重载”。

TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。

function reverse(str:string):string;
function reverse(arr:any[]):any[];
构造函数

构造函数的最大特点,就是必须使用new命令调用。

const d = new Date();

某些函数既是构造函数,又可以当作普通函数使用,比如Date()。这时,类型声明可以写成下面这样。

type F = {
  new (s:string): object;
  (n?:number): number;
}

泛型使用注意

尽量少用泛型

泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。

类型参数越少越好

function filter<
  T,
  Fn extends (arg:T) => boolean
>(
  arr:T[],
  func:Fn
): T[] {
  return arr.filter(func);
}

上面示例有两个类型参数,但是第二个类型参数Fn是不必要的,完全可以直接写在函数参数的类型声明里面。

function filter<T>(
  arr:T[],
  func:(arg:T) => boolean
): T[] {
  return arr.filter(func);
}

类型参数需要出现两次

如果类型参数在定义后只出现一次,那么很可能是不必要的。也就是说,只有当类型参数用到两次或两次以上,才是泛型的适用场合。

泛型可以嵌套

type OrNull<T> = T|null;
type OneOrMany<T> = T|T[];
type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>;

上面示例中,最后一行的泛型OrNull的类型参数,就是另一个泛型OneOrMany

Enum 类型

Enum 是 TypeScript 新增的一种数据结构和类型,中文译为“枚举”

实际开发中可能会定义一些常量,例如颜色RED、GREEN、BLUE。TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。

enum Color {
  Red,     // 0
  Green,   // 1
  Blue     // 2
}

上面示例声明了一个 Enum 结构Color,里面包含三个成员RedGreenBlue。第一个成员的值默认为整数0,第二个为1,第二个为2,以此类推。

使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。

let c = Color.Green; // 1
// 等同于
let c = Color['Green']; // 1

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

let c:Color = Color.Green; // 正确
let c:number = Color.Green; // 正确

Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。

// 编译前
enum Color {
  Red,     // 0
  Green,   // 1
  Blue     // 2
}
// 编译后
let Color = {
  Red: 0,
  Green: 1,
  Blue: 2
};

由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。

Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

缺点

Enum 作为类型有一个缺点,就是输入任何数值都不报错。

enum Bool {
  No,
  Yes
}
function foo(noYes:Bool) {
  // ...
}
foo(33);  // 不报错

上面代码中,函数foo的参数noYes只有两个可用的值,但是输入任意数值,编译都不会报错。

由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)

成员的值

Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2……

enum Color {
  Red,
  Green,
  Blue
}
// 等同于
enum Color {
  Red = 0,
  Green = 1,
  Blue = 2
}

成员的值可以是任意数值,但不能是大整数(Bigint)。

enum Color {
  Red = 90,
  Green = 0.5,
  Blue = 7n // 报错
}

成员的值甚至可以相同。

enum Color {
  Red = 0,
  Green = 0,
  Blue = 0
}

如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。

enum Color {
  Red = 7,
  Green,  // 8
  Blue   // 9
}
// 或者
enum Color {
  Red, // 0
  Green = 7,
  Blue // 8
}

Enum 成员值都是只读的,不能重新赋值。了让这一点更醒目,通常会在 enum 关键字前面加上const修饰,表示这是常量,不能再次赋值。

const enum Color {
  Red,
  Green,
  Blue
}

加上const还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。

const enum Color {
  Red,
  Green,
  Blue
}
const x = Color.Red;
const y = Color.Green;
const z = Color.Blue;
// 编译后
const x = 0 /* Color.Red */;
const y = 1 /* Color.Green */;
const z = 2 /* Color.Blue */;

上面示例中,由于 Enum 结构前面加了const关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。

同名 Enum 的合并

多个同名的 Enum 结构会自动合并。

enum Foo {
  A,
}
enum Foo {
  B = 1,
}
enum Foo {
  C = 2,
}
// 等同于
enum Foo {
  A,
  B = 1,
  C = 2
}

Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。

enum Foo {
  A,
}
enum Foo {
  B, // 报错
}
// 上面示例中,Foo的两段定义的第一个成员,都没有设置初始值,导致报错。

同名 Enum 合并时,不能有同名成员,否则报错。

enum Foo {
  A,
  B
}
enum Foo {
  B = 1, // 报错
  C
}

同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。

// 正确
enum E {
  A,
}
enum E {
  B = 1,
}
// 正确
const enum E {
  A,
}
const enum E {
  B = 1,
}
// 报错
enum E {
  A,
}
const enum E2 {
  B = 1,
}

同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。

字符串 Enum

Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

注意:字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。

enum Foo {
  A, // 0
  B = 'hello',
  C // 报错
}

A之前没有其他成员,所以可以不设置初始值,默认等于0C之前有一个字符串成员,必须C必须有初始值,不赋值就报错了。

Enum 成员可以是字符串和数值混合赋值。除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。

变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。

enum MyEnum {
  One = 'One',
  Two = 'Two',
}
let s = MyEnum.One;
s = 'One'; // 报错,变量s的类型是MyEnum,再赋值为字符串就报错。

由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。

enum MyEnum {
  One = 'One',
  Two = 'Two',
}
function f(arg:MyEnum) {
  return 'arg is ' + arg;
}
f('One') // 报错

字符串 Enum 作为一种类型,有限定函数参数的作用

字符串 Enum 可以使用联合类型(union)代替。

function move(
  where:'Up'|'Down'|'Left'|'Right'
) {
  // ...
 }

上面示例中,函数参数where属于联合类型,效果跟指定为字符串 Enum 是一样的。

注意,字符串 Enum 的成员值,不能使用表达式赋值。

enum MyEnum {
  A = 'one',
  B = ['T', 'w', 'o'].join('') // 报错
}

keyof 运算符

keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。

enum MyEnum {
  A = 'a',
  B = 'b'
}
// 'A'|'B'
type Foo = keyof typeof MyEnum;

上面示例中,keyof typeof MyEnum可以取出MyEnum的所有成员名,所以类型Foo等同于联合类型'A'|'B'

注意,这里的typeof是必需的,否则keyof MyEnum相当于keyof number

type Foo = keyof MyEnum;
// "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"

上面示例中,类型Foo等于类型number的所有原生属性名组成的联合类型。

这是因为 Enum 作为类型,本质上属于numberstring的一种变体,而typeof MyEnum会将MyEnum当作一个值处理,从而先其转为对象类型,就可以再用keyof运算符返回该对象的所有属性名。

如果要返回 Enum 所有的成员值,可以使用in运算符。

enum MyEnum {
  A = 'a',
  B = 'b'
}
// { a:any, b: any }
type Foo = { [key in MyEnum]: any };

反向映射

数值 Enum 存在反向映射,即可以通过成员值获得成员名。

enum Weekdays {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}
console.log(Weekdays[3]) // Wednesday

上面示例中,Enum 成员Wednesday的值等于3,从而可以从成员值3取到对应的成员名Wednesday,这就叫反向映射。

这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。

var Weekdays;
(function (Weekdays) {
    Weekdays[Weekdays["Monday"] = 1] = "Monday";
    Weekdays[Weekdays["Tuesday"] = 2] = "Tuesday";
    Weekdays[Weekdays["Wednesday"] = 3] = "Wednesday";
    Weekdays[Weekdays["Thursday"] = 4] = "Thursday";
    Weekdays[Weekdays["Friday"] = 5] = "Friday";
    Weekdays[Weekdays["Saturday"] = 6] = "Saturday";
    Weekdays[Weekdays["Sunday"] = 7] = "Sunday";
})(Weekdays || (Weekdays = {}));

上面代码中,实际进行了两组赋值,以第一个成员为例。

Weekdays[
  Weekdays["Monday"] = 1
] = "Monday";
// 上面代码有两个赋值运算符(=),实际上等同于下面的代码。
Weekdays["Monday"] = 1;
Weekdays[1] = "Monday";

注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。

类型断言

对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。

type T = 'a'|'b'|'c';
let foo = 'a';
let bar:T = foo; // 报错

上面示例中,最后一行报错,原因是 TypeScript 推断变量foo的类型是string,而变量bar的类型是'a'|'b'|'c',前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。

TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。

type T = 'a'|'b'|'c';
let foo = 'a';
let bar:T = foo as T; // 正确

类型断言有两种语法。

// 语法一:<类型>值
<Type>value
// 语法二:值 as 类型
value as Type

上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。

// 语法一
let bar:T = <T>foo;
// 语法二
let bar:T = foo as T;

类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。

const data:object = {
  a: 1,
  b: 2,
  c: 3
};
data.length; // 报错
(data as Array<string>).length; // 正确

上面示例中,变量data是一个对象,没有length属性。但是通过类型断言,可以将它的类型断言为数组,这样使用length属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。

类型断言的一大用处是,指定 unknown 类型的变量的具体类型。

类型断言的条件

类型断言并不意味着,可以把某个值断言为任意类型。

const n = 1;
const m:string = n as string; // 报错

类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。

expr as T

上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:exprT的子类型,或者Texpr的子类型。

也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型

但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。

// 或者写成 <T><unknown>expr
expr as unknown as T
as const 断言

如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。

TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。

let s = 'JavaScript' as const;
s = 'Python'; // 报错

使用了as const断言以后,let 变量就不能再改变值了。

注意, as const断言只能用于字面量,不能用于变量。

let s = 'JavaScript';
setLang(s as const); // 报错
断言函数

断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。

function isString(value) {
  if (typeof value !== 'string')
    throw new Error('Not a string');
}

上面示例中,函数isString()就是一个断言函数,用来保证参数value是一个字符串。

为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。

function isString(value:unknown):asserts value is string {
  if (typeof value !== 'string')
    throw new Error('Not a string');
}

上面示例中,函数isString()的返回值类型写成asserts value is string,其中assertsis都是关键词,value是函数的参数名,string是函数参数的预期类型。它的意思是,该函数用来断言参数value的类型是string,如果达不到要求,程序就会在这里中断。

装饰器

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。

在语法上,装饰器有如下几个特征。

(1)第一个字符(或者说前缀)是@,后面是一个表达式。

(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。

(3)这个函数接受所修饰对象的一些相关值作为参数。

(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

下面就是一个最简单的装饰器。

function simpleDecorator() {
  console.log('hi');
}
@simpleDecorator
class A {} // "hi"

上面示例中,函数simpleDecorator()用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志。

装饰器有多种形式,基本上只要在@符号后面添加表达式都是可以的。下面都是合法的装饰器。

@myFunc
@myFuncFactory(arg1, arg2)
​
@libraryModule.prop
@someObj.method(123)
​
@(wrap(dict['prop']))

类装饰器

类装饰器的类型描述如下。

type ClassDecorator = (
  value: Function,
  context: {
    kind: 'class';
    name: string | undefined;
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class

类装饰器一般用来对类进行操作,可以不返回任何值,请看下面的例子。

function Greeter(value, context) {
  if (context.kind === 'class') {
    value.prototype.greet = function () {
      console.log('你好');
    };
  }
}
@Greeter
class User {}
let u = new User();
u.greet(); // "你好"

上面示例中,类装饰器@Greeter在类User的原型对象上,添加了一个greet()方法,实例就可以直接使用该方法。

方法装饰器

方法装饰器用来装饰类的方法(method)。它的类型描述如下。

type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

参数value是方法本身,参数context是上下文对象,有以下属性。

  • kind:值固定为字符串method,表示当前为方法装饰器。

  • name:所装饰的方法名,类型为字符串或 Symbol 值。

  • static:布尔值,表示是否为静态方法。该属性为只读属性。

  • private:布尔值,表示是否为私有方法。该属性为只读属性。

  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。

  • addInitializer():为方法增加初始化函数。

方法装饰器会改写类的原始方法,实质等同于下面的操作。

function trace(decoratedMethod) {
  // ...
}
class C {
  @trace
  toString() {
    return 'C';
  }
}
// `@trace` 等同于
// C.prototype.toString = trace(C.prototype.toString);

如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。

function replaceMethod() {
  return function () {
    return `How are you, ${this.name}?`;
  }
}
class Person {
  constructor(name) {
    this.name = name;
  }
  @replaceMethod
  hello() {
    return `Hi ${this.name}!`;
  }
}
const robin = new Person('Robin');
robin.hello() // 'How are you, Robin?'

属性装饰器

属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。

type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown, set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => (initialValue: unknown) => unknown | void;

注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的。

属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。

function logged(value, context) {
  const { kind, name } = context;
  if (kind === 'field') {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }
}
class Color {
  @logged name = 'green';
}
const color = new Color();
// "initializing name with value green"

属性装饰器的返回值函数,可以用来更改属性的初始值。

function twice() {
  return initialValue => initialValue * 2;
}
class C {
  @twice
  field = 3;
}
const inst = new C();
inst.field // 6

getter 装饰器,setter 装饰器

getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。它们的类型描述如下。

type ClassGetterDecorator = (
  value: Function,
  context: {
    kind: 'getter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;
type ClassSetterDecorator = (
  value: Function,
  context: {
    kind: 'setter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

注意,getter 装饰器的上下文对象contextaccess属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。

这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。

class C {
  @lazy
  get value() {
    console.log('正在计算……');
    return '开销大的计算结果';
  }
}
function lazy(
  value:any,
  {kind, name}:any
) {
  if (kind === 'getter') {
    return function (this:any) {
      const result = value.call(this);
      Object.defineProperty(
        this, name,
        {
          value: result,
          writable: false,
        }
      );
      return result;
    };
  }
  return;
}
const inst = new C();
inst.value
// 正在计算……
// '开销大的计算结果'
inst.value
// '开销大的计算结果'

上面示例中,第一次读取inst.value,会进行计算,然后装饰器@lazy将结果存入只读属性value,后面再读取这个属性,就不会进行计算了。

accessor 装饰器

装饰器语法引入了一个新的属性修饰符accessor

class C {
  accessor x = 1;
}

上面示例中,accessor修饰符等同于为属性x自动生成取值器和存值器,它们作用于私有属性x。也就是说,上面的代码等同于下面的代码。

class C {
  #x = 1;
  get x() {
    return this.#x;
  }
  set x(val) {
    this.#x = val;
  }
}
​
//accessor 装饰器的类型如下。
​
type ClassAutoAccessorDecorator = (
  value: {
    get: () => unknown;
    set(value: unknown) => void;
  },
  context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    static: boolean;
    private: boolean;
    addInitializer(initializer: () => void): void;
  }
) => {
  get?: () => unknown;
  set?: (value: unknown) => void;
  init?: (initialValue: unknown) => unknown;
} | void;

accessor 装饰器的value参数,是一个包含get()方法和set()方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()方法和set()方法。此外,装饰器返回的对象还可以包括一个init()方法,用来改变私有属性的初始值。

下面是一个例子

class C {
  @logged accessor x = 1;
}
function logged(value, { kind, name }) {
  if (kind === "accessor") {
    let { get, set } = value;
    return {
      get() {
        console.log(`getting ${name}`);
        return get.call(this);
      },
      set(val) {
        console.log(`setting ${name} to ${val}`);
        return set.call(this, val);
      },
      init(initialValue) {
        console.log(`initializing ${name} with value ${initialValue}`);
        return initialValue;
      }
    };
  }
}
let c = new C();
c.x;
// getting x
c.x = 123;
// setting x to 123

装饰器@logged为属性x的存值器和取值器,加上了日志输出。

装饰器的执行顺序

装饰器的执行分为两个阶段。

(1)评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。

(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。

也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。

应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。

请看下面例子

function d(str:string) {
  console.log(`评估 @d(): ${str}`);
  return (
    value:any, context:any
  ) => console.log(`应用 @d(): ${str}`);
}
function log(str:string) {
  console.log(str);
  return str;
}
@d('类装饰器')
class T {
  @d('静态属性装饰器')
  static staticField = log('静态属性值');
  @d('原型方法')
  [log('计算方法名')]() {}
  @d('实例属性')
  instanceField = log('实例属性值');
}

上面示例中,类T有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。它的运行结果如下。

// "评估 @d(): 类装饰器"
// "评估 @d(): 静态属性装饰器"
// "评估 @d(): 原型方法"
// "计算方法名"
// "评估 @d(): 实例属性"
// "应用 @d(): 原型方法"
// "应用 @d(): 静态属性装饰器"
// "应用 @d(): 实例属性"
// "应用 @d(): 类装饰器"
// "静态属性值"

可以看到,类载入的时候,代码按照以下顺序执行。

(1)装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。

注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。

(2)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。

原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。

注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。

如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @bound
  @log
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

上面示例中,greet()有两个装饰器,内层的@log先执行,外层的@bound针对得到的结果再执行。

类型运算

改变成员类型的顺序不影响联合类型的结果类型。

type T0 = string | number;
type T1 = number | string;

对部分类型成员使用分组运算符不影响联合类型的结果类型。

type T0 = (boolean | string) | number;
type T1 = boolean | (string | number);

联合类型的成员类型可以进行化简。假设有联合类型“U = T0 | T1”,如果T1是T0的子类型,那么可以将类型成员T1从联合类型U中消去。最后,联合类型U的结果类型为“U = T0”。例如,有联合类型“boolean | true | false”。其中,true类型和false类型是boolean类型的子类型,因此可以将true类型和false类型从联合类型中消去。最终,联合类型“boolean | true | false”的结果类型为boolean类型。

type T0 = boolean | true | false;
// 所以T0等同于 T1
type T1 = boolean;
优先级

&的优先级高于|

A & B | C & D
// 该类型等同于如下类型:
(A & B) | (C & D)

分配律

A & (B | C) 
// 等同于
(A & B) | (A & C)
(A | B) & (C | D) ≡ A & C | A & D | B & C | B & D

never

never 可以视为空集。

type NeverIntersection = never & string; // Type: never
type NeverUnion = never | string; // Type: string

很适合在交叉类型中用作过滤。

type OnlyStrings<T> = T extends string ? T : never;
type RedOrBlue = OnlyStrings<"red" | "blue" | 0 | false>;
// Equivalent to: "red" | "blue"

类型映射

映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。

举例来说,现有一个类型A和另一个类型B

type A = {
  foo: number;
  bar: number;
};
type B = {
  foo: string;
  bar: string;
};

上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,逐个写起来就很麻烦。

使用类型映射,就可以从类型A得到类型B

type A = {
  foo: number;
  bar: number;
};
type B = {
  [prop in keyof A]: string;
};

上面示例中,类型B采用了属性名索引的写法,[prop in keyof A]表示依次得到类型A的所有属性名,然后将每个属性的类型改成string

键名重映射

TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。

type A = {
  foo: number;
  bar: number;
};
type B = {
  [p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
  fooID: number;
  barID: number;
};

上面示例中,类型B是类型A的映射,但在映射时把属性名改掉了,在原始属性名后面加上了字符串ID

下面是另一个例子。

interface Person {
  name: string;
  age: number;
  location: string;
}
type Getters<T> = {
  [P in keyof T
    as `get${Capitalize<string & P>}`]: () => T[P];
};
type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
  getName: () => string;
  getAge: () => number;
  getLocation: () => string;
}

上面示例中,类型LazyPerson是类型Person的映射,并且把键名改掉了。

总结

这篇文章主要是对上一篇文章的补充,介绍了更多typescript中细节的内容,这些内容在日常中可能用的并不是很频繁,但是学习了解一下也是十分有意思的,拓宽自己的知识面。最后也是感谢大家看到这里,麻烦点个赞!发大财!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值