Skip to content

TypeScript 学习

any , unknown , never 类型

  • any 类型

    类型表示没有任何限制,该类型的变量可以赋予任意类型的值
    any 类型除了关闭类型检查,还会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错

  • unknown 类型

    为了解决 any 类型“污染”其他变量的问题;
    unknown 跟 any 的相似之处,在于所有类型的值都可以分配给 unknown 类型
    不能直接调用 unknown 类型变量的方法和属性

  • never 类型

    为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念
    该类型为空,不包含任何值

TypeScript 的类型系统

  • TypeScript 继承了 JavaScript 的类型,这个基础上,定义了一套自己的类型系统
  1. 基本类型

    boolean、string、number、bigint、symbol、object、undefined、null

  2. 包装对象类型

    boolean、string、number、bigint、symbol 这五种原始类型的值,都有对应的包装对象

    Boolean、String、Number、BigInt、Symbol 大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象

  3. Object 类型与 object 类型

    大写 Object 类型,这囊括了几乎所有的值; 除了 undefined 和 null 这两个值不能转为对象,其他任何值(原始类型值、对象、数组、函数)都可以赋值给 Object 类型;另外空对象{}是 Object 类型的简写形式,所以使用 Object 时常常用空对象代替

    小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数不包括原始类型的值

  4. undefined 和 null

    undefined 和 null 既是值,又是类型

    任何其他类型的变量都可以赋值为 undefined 或 null

  5. 值类型

    单个值也是一种类型,称为“值类型”

    const 命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型

    注意,const 命令声明的变量,如果赋值为对象,并不会推断为值类型

ts
const x = "https"; // x 的类型是 "https"

const y: string = "https"; // y 的类型是 string

const x = { foo: 1 }; // x 的类型是 { foo: number }
  1. 联合类型

    联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示

    联合类型 A|B 表示,任何一个类型只要属于 A 或 B,就属于联合类型 A|B

    如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”,区分该值到底属于哪一种类型,然后再进一步处理

  2. 交叉类型

    交叉类型指的多个类型组成的一个新类型,使用符号&表示

    交叉类型 A&B 表示,任何一个类型必须同时属于 A 和 B,才属于交叉类型 A&B,即交叉类型同时满足 A 和 B 的特征

  3. type 命令

    type 命令用来定义一个类型的别名

  4. typeof 运算符

JavaScript 里面,typeof 运算符只可能返回八种结果,而且都是字符串

TypeScript 将 typeof 运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型

  1. 块级类型声明

    TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效

ts
if (true) {
  type T = number;
  let v: T = 5;
} else {
  type T = string;
  let v: T = "hello";
}

TypeScript 数组,元组类型

  • 数组:

    所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员

    TypeScript 允许使用方括号读取数组成员的类型

    TypeScript 将 readonly number[]与 number[]视为两种不一样的类型,后者是前者的子类型

    只读数组声明方式

    ts
    const a1: readonly number[] = [1, 2, 3];
    
    const a2: ReadonlyArray<number> = [0, 1];
    
    const a3: Readonly<number[]> = [0, 1];
    
    const a4 = [0, 1] as const;
    • 元组

    元组是 TypeScript 特有的数据类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同

    数组的成员类型写在方括号外面(number[]),元组的成员类型是写在方括号里面([number])

ts
// 数组
let a: number[] = [1];

// 元组
let t: [number] = [1];

TypeScript 的 symbol 类型

  • symbol 类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值
  • 为了解决这个问题,TypeScript 设计了 symbol 的一个子类型 unique symbol,它表示单个的、某个具体的 Symbol 值
  • unique symbol 表示单个值,所以这个类型的变量是不能修改值的,只能用 const 命令声明,不能用 let 声明
ts
// 正确
const x: unique symbol = Symbol();

// 报错
let y: unique symbol = Symbol();

TypeScript 的函数类型

  1. 函数类型写法
ts
// 写法一
const hello = function (txt: string) {
  console.log("hello " + txt);
};

// 写法二
const hello: (txt: string) => void = function (txt) {
  console.log("hello " + txt);
};

// 写法三:对象的写法
let add: {
  (x: number, y: number): number;
};

add = function (x, y) {
  return x + y;
};
  1. rest 参数
  • 表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)
ts
// rest 参数为数组
function joinNumbers(...nums: number[]) {}

// rest 参数为元组
function f(...args: [boolean, number]) {}
  1. 高阶函数
  • 一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数
ts
(someValue: number) => (multiplier: number) => someValue * multiplier;
  1. 函数重载

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

ts
function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(stringOrArray: string | any[]): string | any[] {
  if (typeof stringOrArray === "string")
    return stringOrArray.split("").reverse().join("");
  else return stringOrArray.slice().reverse();
}

上面示例分别对函数 reverse()的两种参数情况,给予了类型声明。但是后面还必须对函数 reverse()给予完整的类型声明

重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系

ts
// 写法一
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any): number {
  return x.length;
}

// 写法二
function len(x: any[] | string): number {
  return x.length;
}
  1. 构造函数

构造函数的类型写法,就是在参数列表前面加上 new 命令

ts
class Animal {
  numLegs: number = 4;
}

type AnimalConstructor = new () => Animal;

function create(c: AnimalConstructor): Animal {
  return new c();
}

const a = create(Animal);

构造函数采用对象形式写法

ts
type F = {
  new (s: string): object;
};

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

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

TypeScript 的对象类型

  1. 对象类型的声明方法

    就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型

    注意 1: 可选属性与允许设为 undefined 的必选属性是不等价的

    注意 2: readonly 如果属性值是一个对象,readonly 修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象

ts
const obj: {
  x: number;
  y?: number; // 可选属性
  readonly prop: number; // 只读属性
} = {
  x: 1,
  y: undefined,
  age: 20,
};
  1. 属性名的索引类型

    如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”

ts
// 索引类型里面,最常见的就是属性名的字符串索引
type MyObj = {
  [property: string]: string;
};
  1. 解构赋值

解构赋值的类型写法,跟为对象声明类型是一样的 注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途

ts
const {
  id,
  name,
  price,
}: {
  id: string;
  name: string;
  price: number;
} = product;

// 注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途
let { x: foo, y: bar } = obj;

// 等同于
let foo = obj.x;
let bar = obj.y;

// 如果要为x和y指定类型,不得不写成下面这样
let { x: foo, y: bar }: { x: string; y: number } = obj;
  1. 结构类型原则

只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则

ts
type A = {
  x: number;
};

type B = {
  x: number;
  y: number;
};

// 上面示例中,对象A只有一个属性x,类型为number。对象B满足这个特征,
// 因此兼容对象A,只要可以使用A的地方,就可以使用B
  1. 严格字面量检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错

ts
const point: {
  x: number;
  y: number;
} = {
  x: 1,
  y: 1,
  z: 1, // 报错
};

规避严格字面量检查

ts
type Options = {
  title: string;
  darkMode?: boolean;
};

const obj: Options = {
  title: "我的网页",
  darkmode: true, // 报错: 属性darkMode拼写错了,成了darkmode。如果没有严格字面量规则,就不会报错,因为darkMode是可选属性,根据结构类型原则,任何对象只要有title属性,都认为符合Options类型
};

// 可以使用中间变量
let myOptions = {
  title: "我的网页",
  darkmode: true,
};

const obj: Options = myOptions;
  1. 最小可选属性规则

    根据“结构类型”原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的;为了避免这种情况,TypeScript 引入了一个“最小可选属性规则”,也称为“弱类型检测”

    如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”

ts
type Options = {
  a?: number;
  b?: number;
  c?: number;
};

const opts = { d: 123 };

const obj: Options = opts; // 报错
  1. 空对象

    空对象是 TypeScript 的一种特殊值,也是一种特殊类型

ts
const obj = {};
obj.prop = 123; // 报错

// 变量obj的值是一个空对象,然后对obj.prop赋值就会报错。原因是这时 TypeScript 会推断变量obj的类型为空对象,实际执行的是下面的代码。

const obj: {} = {};
ts
let d: {};
// 等同于
// let d:Object;

d = {};
d = { x: 1 };
d = "hello";
d = 2;

// 因为Object可以接受各种类型的值,而空对象是Object类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性

TypeScript 的 interface 接口

  • interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。
  • 使用了某个模板的对象,就拥有了指定的类型结构

1. interface 的简介

  • interface 可以表示对象的各种语法,它的成员有 5 种形式。

    • 对象属性
    • 对象的属性索引

      属性索引共有 string、number 和 symbol 三种类型。一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性

    • 对象方法
    • 函数
    • 构造函数
ts
interface Demo {
  x: number; // 对象属性

  [prop: number]: number; //对象的属性索引

  // 对象的方法共有三种写法
  f1(x: boolean): string;
  f2: (x: boolean) => string;
  f3: { (x: boolean): string };
}

// interface 也可以用来声明独立的函数。
interface Add {
  (x: number, y: number): number;
}

const myAdd: Add = (x, y) => x + y;

// interface 内部可以使用new关键字,表示构造函数。
interface ErrorConstructor {
  new (message?: string): Error;
}

2. interface 的继承

  • interface 可以使用 extends 关键字,继承其他 interface
  • 注意:多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错
ts
interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}
  • interface 可以继承 type 命令定义的对象类型
ts
type Country = {
  name: string;
  capital: string;
};

interface CountryWithPop extends Country {
  population: number;
}

// CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性
  • interface 还可以继承 class,即继承该类的所有成员
ts
class A {
  x: string = "";

  y(): boolean {
    return true;
  }
}

interface B extends A {
  z: number;
}
// B继承了A,因此B就具有属性x、y()和z

接口合并

  • 多个同名接口会合并成一个接口
  • 这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便
ts
interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number;
}

// 两个Box接口会合并成一个接口,同时有height、width和length三个属性

interface 与 type 的异同

  • 相同点

    • 表现在都能为对象类型起名
  • 差异

    1. type 能够表示非对象类型,而 interface 只能表示对象类型(包括数组、函数等)
    2. interface 可以继承其他类型,type 不支持继承
    3. 同名 interface 会自动合并,同名 type 则会报错
    4. interface 不能包含属性映射(mapping),type 可以
    ts
      // 正确
      type PointCopy1 = {
          [Key in keyof Point]: Point[Key];
      };
    
      // 报错
      interface PointCopy2 {
          [Key in keyof Point]: Point[Key];
      };
    1. this 关键字只能用于 interface
    ts
    // 正确
    interface Foo {
      add(num: number): this;
    }
    
    // 报错
    type Foo = {
      add(num: number): this;
    };
    1. type 可以扩展原始数据类型,interface 不行
    ts
    // 正确
    type MyStr = string & {
      type: "new";
    };
    
    // 报错
    interface MyStr extends string {
      type: "new";
    }
  1. interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以

如果有复杂的类型运算,那么没有其他选择只能使用 type; 一般情况下,interface 灵活性比较高,便于扩充类型或自动合并,建议优先使用

TypeScript 的 class 类型

属性的类型

    1. 类的属性可以在顶层声明,也可以在构造方法内部声明
ts
class Point {
  x: number;
  y: number;
  z!: number; // 类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言
  readonly id = "foo"; // 属性名前面加上 readonly 修饰符,就表示该属性是只读的

  // 类的方法就是普通函数,类型声明方式与函数一致
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
    1. 类的 存取器方法:存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法

    如果某个属性只有 get 方法,没有 set 方法,那么该属性自动成为只读属性

    set 方法的参数类型,必须兼容 get 方法的返回值类型,否则报错

    get 方法与 set 方法的可访问性必须一致,要么都为公开方法,要么都为私有方法

    1. 类的 属性索引

      注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型

ts
// [s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  get(s: string) {
    return this[s] as boolean;
  }
}

类的 interface 接口

    1. interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制
ts
interface Country {
  name: string;
  capital: string;
  get(name: string): boolean;
}
// 或者
type Country = {
  name: string;
  capital: string;
  get(name: string): boolean;
};

class MyCountry implements Country {
  name = "";
  capital = "";
  z: number = 1; // 类可以定义接口没有声明的方法和属性

  //  类 MyCountry 实现了接口 Country,但是后者并不能代替 MyCountry 的类型声明。因此, MyCountry 的get()方法的参数s的类型是any,而不是string。 MyCountry 类依然需要声明参数s的类型
  get(s) {
    // s 的类型是 any
    return true;
  }
}

// interface或type都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。

// 注意: interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明
  • implements 关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口
ts
class Car {
  id: number = 1;
  move(): void {}
}

class MyCar implements Car {
  id = 2; // 不可省略
  move(): void {} // 不可省略
}

// 实现多个接口
class Car implements MotorVehicle, Flyable, Swimmable {
  // ...
}

类的自身类型

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符

类的继承

  • 类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法
ts
class A {
  greet() {
    console.log("Hello, world!");
  }
}

class B extends A {}

const b = new B();
b.greet(); // "Hello, world!"

// 子类可以覆盖基类的同名方法
// 但是,子类的同名方法不能与基类的类型定义相冲突
class B extends A {
  // 子类B定义了一个方法greet(),覆盖了基类A的同名方法。
  // 其中,参数name省略时,就调用基类A的greet()方法,
  // 这里可以写成super.greet(),使用super关键字指代基类是常见做法
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符控制:public、private 和 protecte

  1. public 修饰符表示这是公开成员,外部可以自由访问
  2. private 修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员
  3. protected 修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用

静态成员

类的内部可以使用 static 关键字,定义静态成员。 静态成员是只能通过类本身使用的成员,不能通过实例对象使用

ts
class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}

MyClass.x; // 0
MyClass.printX(); // 0

泛型类

类也可以写成泛型,使用类型参数

ts
class Box<Type> {
  contents: Type;

  constructor(value: Type) {
    this.contents = value;
  }
}

const b: Box<string> = new Box("hello!");

抽象类,抽象成员

TypeScript 允许在类的定义前面,加上关键字 abstract,表示该类不能被实例化,只能当作其他类的模板

ts
// 抽象类只能当作基类使用,用来在它的基础上定义子
abstract class A {
  id = 1;
}

class B extends A {
  amount = 100;
}

const b = new B();

b.id; // 1
b.amount; // 100

// 抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类
abstract class C extends A {
  bar: string;
}

abstract class D {
  // 属性名和方法名有abstract关键字,表示该方法需要子类实现。
  // 如果子类没有实现抽象成员,就会报错
  abstract foo: string;
  bar: string = "";
}

class E extends D {
  foo = "b";
}

注意点:

  1. 抽象成员只能存在于抽象类,不能存在于普通类。
  2. 抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加 abstract 关键字。
  3. 抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。
  4. 一个子类最多只能继承一个抽象类

this 问题

类的方法经常用到 this 关键字,它表示该方法当前所在的对象

  • 注意,this 类型不允许应用于静态成员 : 原因是 this 类型表示实例对象,但是静态成员拿不到实例对象
ts
// 有些场合需要给出this类型,但是 JavaScript 函数通常不带有this参数,
// 这时 TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,
// 用来描述函数内部的this关键字的类型

// 编译前
function fn(this: SomeType, x: number) {}

// 编译后
function fn(x) {}

// eg:
class A {
  name = "A";

  getName(this: A) {
    return this.name;
  }
}

const a = new A();
const b = a.getName;

b(); // 报错

TypeScript 泛型

有些时候,函数返回值的类型与参数类型是相关的, 为了解决这个问题,TypeScript 就引入了“泛型”(generics), 泛型的特点就是带有“类型参数”

泛型的写法

泛型主要用在四个场合:函数、接口、类和别名

ts
// 函数的泛型写法
// 写法一
let myId: <T>(arg: T) => T = id;

// 写法二
let myId: { <T>(arg: T): T } = id;

// 接口的泛型写法
interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

// 类的泛型写法
class Pair<K, V> {
  key: K;
  value: V;
}

// 类型别名的泛型写法
type Nullble<T> = T | undefined | null;
F;

类型参数的默认值

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值

ts
function getFirst<T = string>(arr: T[]): T {
  return arr[0];
}

类型参数的约束条件

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明

类型参数的约束条件采用下面的形式。

ts
<TypeParameter extends ConstraintType>
ts
// 类型参数A和B都有约束条件,并且B还有默认值。
// 所以,调用Fn的时候,可以只给出A的值,不给出B的值
type Fn<A extends string, B extends string = "world"> = [A, B];

type Result = Fn<"hello">; // ["hello", "world"]

使用注意点

  1. 尽量少用泛型。泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用
  2. 类型参数越少越好;多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好
  3. 类型参数需要出现两次。如果类型参数在定义后只出现一次,那么很可能是不必要的
ts
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
  1. 泛型可以嵌套。类型参数可以是另一个泛型
ts
type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

TypeScript 的 Enum 类型

开发中,经常需要定义一组相关的常量,TypeScript 设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用

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

很大程度上,Enum 结构可以被对象的 as const 断言替代

ts
// 编译前
enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}

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

同名 Enum 的合并

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

注意点:

  1. Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错
  2. 同名 Enum 合并时,不能有同名成员,否则报错
ts
enum Foo {
  A,
}

enum Foo {
  B = 1,
}

enum Foo {
  C = 2,
}

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

字符串 Enum

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

  1. 字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前
  2. 字符串 Enum 的成员值,不能使用表达式赋值
ts
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

keyof 运算符

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

ts
enum MyEnum {
  A = "a",
  B = "b",
}

// 'A'|'B'
type Foo = keyof typeof MyEnum;
// 注意,这里的typeof是必需的,否则keyof MyEnum相当于keyof string

反向映射

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

ts
enum Weekdays {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday,
}

console.log(Weekdays[3]); // Wednesday

TypeScript 的类型断言

TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型;

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

类型断言有两种语法。

ts
// 语法一:<类型>值
<Type>value;

// 语法二:值 as 类型
value as Type;

类型断言的条件

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

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

ts
// 或者写成 <T><unknown>expr
expr as unknown as T;

// eg:
const n = 1;
const m: string = n as unknown as string; // 正确

as const 断言

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

ts
const v1 = {
  x: 1,
  y: 2,
}; // 类型是 { x: number; y: number; }

const v2 = {
  x: 1 as const,
  y: 2,
}; // 类型是 { x: 1; y: number; }

const v3 = {
  x: 1,
  y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }

// a1 的类型推断为 number[]
const a1 = [1, 2, 3];

// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;

非空断言

对于那些可能为空的变量(即可能等于 undefined 或 null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

ts
function f(x?: number | null) {
  validateNumber(x); // 自定义函数,确保 x 是数值
  console.log(x!.toFixed());
}

function validateNumber(e?: number | null) {
  if (typeof e !== "number") throw new Error("Not a number");
}

断言函数

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

注意点:

  1. 断言函数的 asserts 语句等同于 void 类型,所以如果返回除了 undefined 和 null 以外的值,都会报错
  2. 函数返回值的断言写法,只是用来更清晰地表达函数意图,真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错
ts
// 函数isString()的返回值类型写成asserts value is string,
// 其中asserts和is都是关键词,value是函数的参数名,string是函数参数的预期类型。
// 它的意思是,该函数用来断言参数value的类型是string,如果达不到要求,程序就会在这里中断
function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string");
}

// 注意,函数返回值的断言写法,只是用来更清晰地表达函数意图,
// 真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错
function isString(value: unknown): asserts value is string {
  if (typeof value !== "number") throw new Error("Not a number");
}

// 断言函数的asserts语句等同于void类型,
// 所以如果返回除了undefined和null以外的值,都会报错
function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string");
  return true; // 报错
}

如果要断言某个参数保证为真(即不等于 false、undefined 和 null),TypeScript 提供了断言函数的一种简写形式

ts
function assert(x: unknown): asserts x {
  if (!x) {
    throw new Error(`${x} should be a truthy value.`);
  }
}

TypeScript namespace

namespace 是一种将相关代码组织在一起的方式,中文译为“命名空间”。

它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace

ts
// 1. namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。
namespace Utils {
  function isString(value: any) {
    return typeof value === "string";
  }

  // 正确
  isString("yes");
}

Utils.isString("no"); // 报错

// 2. 只要加上export前缀,就可以在命名空间外部使用内部成员
namespace Utility {
  export function log(msg: string) {
    console.log(msg);
  }
  export function error(msg: string) {
    console.error(msg);
  }
}

Utility.log("Call me");
Utility.error("maybe!");

TypeScript 装饰器 TODO

装饰器是一种语法结构,用来在定义时修改类(class)的行为。在语法上,装饰器有如下几个特征。

  1. 第一个字符(或者说前缀)是@,后面是一个表达式。
  2. @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
  3. 这个函数接受所修饰对象的一些相关值作为参数。
  4. 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象

TypeScript 类型运算符

keyof 运算符

keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型

JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是 string|number|symbol

ts
interface T {
  0: boolean;
  a: string;
  b(): void;
}

type KeyT = keyof T; // 0 | 'a' | 'b'

// 1. 对于任意对象的键名的联合类型就是 string | number | symbol
type KeyT = keyof any;

// 2. 没有自定义键名的类型使用 keyof 运算符,
// 返回never类型,表示不可能有这样类型的键名
type KeyT = keyof object; // never

// 3. 对于联合类型,keyof 返回成员共有的键名
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };

type KeyT = keyof (A | B); // 返回 'z'

// 4. 对于交叉类型,keyof 返回所有键名
type A = { a: string; x: boolean };
type B = { b: string; y: number };


type KeyT = keyof (A & B);// 返回 'a' | 'x' | 'b' | 'y'

// 相当于
keyof (A & B) ≡ keyof A | keyof B

in 运算符

JavaScript 语言中,in 运算符用来确定对象是否包含某个属性名

ts
const obj = { a: 123 };

if ("a" in obj) console.log("found a");

type U = "a" | "b" | "c";

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number;
  b: number;
  c: number;
};

方括号运算符

方括号运算符([])用于取出对象的键值类型,比如 T[K]会返回对象 T 的属性 K 的类型。

ts
type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// Age 的类型是 number
type Age = Person["age"];

extends...?: 条件运算符

条件运算符 extends...?:可以根据当前类型是否符合某种条件,返回不同的类型

ts
T extends U ? X : Y

infer 关键字

infer 关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。

它通常跟条件运算符一起使用,用在 extends 关键字后面的父类型之中

ts
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

is 运算符

函数返回布尔值的时候,可以使用 is 运算符,限定返回值与参数之间的关系。

is 运算符用来描述返回值属于 true 还是 false

ts
// 函数isFish()的返回值类型为pet is Fish,表示如果参数pet类型为Fish,则返回true,否则返回false
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// is运算符特殊用法,就是用在类(class)的内部,描述类的方法的返回值
class Teacher {
  isStudent(): this is Student {
    return false;
  }
}

class Student {
  isStudent(): this is Student {
    return true;
  }
}

模板字符串

ypeScript 允许使用模板字符串,构建类型。模板字符串的最大特点,就是内部可以引用其他类型

ts
type World = "world";

// "hello world"
type Greeting = `hello ${World}`;

satisfies 运算符

satisfies 运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用 satisfies 运算符对其进行检测

ts
type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0], // 报错
} satisfies Record<Colors, string | RGB>;

const greenComponent = palette.green.substring(1); // 不报错

TypeScript 的类型映射

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

ts
// 使用类型映射,就可以从类型A得到类型B
type A = {
  foo: number;
  bar: number;
};

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

映射修饰符

映射会原样复制原始对象的可选属性和只读属性

  1. +修饰符:写成+?或+readonly,为映射属性添加?修饰符或 readonly 修饰符。
  2. –修饰符:写成-?或-readonly,为映射属性移除?修饰符或 readonly 修饰符。
ts
// 添加可选属性
type Optional<Type> = {
  [Prop in keyof Type]+?: Type[Prop];
};

// 移除可选属性
type Concrete<Type> = {
  [Prop in keyof Type]-?: Type[Prop];
};

键名重映射

TypeScript 4.1 引入了键名重映射,允许改变键名

ts
type A = {
  foo: number;
  bar: number;
};

type B = {
  [p in keyof A as `${p}ID`]: number;
};

// 等同于
type B = {
  fooID: number;
  barID: number;
};

键名重映射还可以过滤掉某些属性

ts
type User = {
  name: string;
  age: number;
};

type Filter<T> = {
  [K in keyof T as T[K] extends string ? K : never]: string;
};

type FilteredUser = Filter<User>; // { name: string }

// 映射K in keyof T获取类型T的每一个属性以后,然后使用as Type修改键名。
// 它的键名重映射as T[K] extends string ? K : never],使用了条件运算符。
// 如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never,
// 即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性

Released under the MIT License.