TypeScript进阶

88,610次阅读
没有评论

共计 76717 个字符,预计需要花费 192 分钟才能阅读完成。

Typescript 进阶

基础知识

JavaScript 的核心特点就是灵活,但随着项目规模的增大,灵活反而增加开发者的心智负担。例如在代码中一个变量可以被赋予字符串、布尔、数字、甚至是函数,这样就充满了不确定性。而且这些不确定性可能需要在代码运行的时候才能被发现,所以我们需要类型的约束

当然不可否认的是有了类型的加持多少会影响开发效率,但是可以让大型项目更加健壮

  • Typescript 更像后端 JAVA,让 JS 可以开发大型企业应用;
  • TS 提供的类型系统可以帮助我们在写代码时提供丰富的语法提示;
  • 在编写代码时会对代码进行类型检查从而避免很多线上错误;

越来越多的项目开始拥抱 TS 了,典型的 Vue3、Pinia、第三方工具库、后端 NodeJS 等。我们也经常为了让编辑器拥有更好的支持去编写 **.d.ts 文件 **。

什么是 Typescript

TypeScript 是一门编程语言,TypeScriptJavascript 的超集(任何的 JS 代码都可以看成 TS 代码),同时 Typescript 扩展了 Javascript 语法添加了静态类型支持以及其他一些新特性。

TypeScript 进阶

TypeScript 代码最终会被编译成 JavaScript 代码,以在各种不同的运行环境中执行

环境配置

全局编译 TS 文件

全局安装 typescriptTS进行编译

npm install typescript -g
tsc --init 
tsc 
tsc --watch 
ts-node 执行 TS 文件

采用 vscode code runner 插件 运行文件

npm install ts-node -g

直接右键运行当前文件快速拿到执行结果

配置 rollup 开发环境
  • 安装依赖

    pnpm install rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
    
  • 初始化 TS 配置文件

    npx tsc --init
    
  • rollup配置操作rollup.config.mjs

    import ts from "rollup-plugin-typescript2";
    import { nodeResolve } from "@rollup/plugin-node-resolve";
    import serve from "rollup-plugin-serve";
    import path from "path";
    import { fileURLToPath } from "url";
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    export default {
      input: "src/index.ts",
      output: {
        format: "iife",
        file: path.resolve(__dirname, "dist/bundle.js"),
        sourcemap: true,
      },
      plugins: [
        nodeResolve({
          extensions: [".js", ".ts"],
        }),
        ts({
          tsconfig: path.resolve(__dirname, "tsconfig.json"),
        }),
        serve({
          open: true,
          openPage: "/public/index.html",
          port: "3000",
        }),
      ],
    };
    
  • package.json配置

    "scripts": {
        "start": "rollup -c -w"
    }
    

我们可以通过 npm run start 启动服务来使用 typescript 啦~

常用插件

  • Error Lens 提示错误插件
  • TypeScript 内置配置(code->首选项 ->settings)根据需要打开设置即可

基础类型

TS 中有很多类型:内置的类型(DOM、Promise 等都在 typescript 模块中)基础类型、高级类型、自定义类型。

TS 中冒号后面的都为类型标识,等号后面的都是值。

  • ts 类型要考虑安全性,一切从安全角度上触发。
  • ts 在使用的时候程序还没有运行
  • ts 中有类型推导,会自动根据赋予的值来返回类型,只有无法推到或者把某个值赋予给某个变量的时候我们需要添加类型。
  • 通过 export {}进行模块隔离

TypeScript 进阶

布尔、数字、字符串类型
let name: string = "hs"; 
let age: number = 30;
let handsome: boolean = true;

我们标识类型的时候 原始数据类型全部用小写的类型,如果描述实例类型则用大写类型(大写类型就是 装箱类型 ,其中也包含 拆箱类型),例如下面的大写:String

let s1: string = "abc";
let s2: string = new String("abc"); 
let s3: String = new String("abc");
let s4: String = "abc";

什么是包装对象?

我们在使用原始数据类型时,调用原始数据类型上的方法,默认会将原始数据类型包装成对象类型。

数组

数组 用于储存多个相同类型数据的集合。TypeScript 中有两种方式来声明一个数组类型

let arr1: number[] = [1, 2, 3];
let arr2: string[] = ["1", "2", "3"];
let arr3: (number | string)[] = [1, "2", 3]; 
let arr4: Arraynumber | string> = [1, "2", 3]; 
元组类型

元组的特点就 固定长度 固定类型的一个数组(规定长度和存储的类型)

let tuple1: [string, number, boolean] = ["hh", 30, true];
tuple[3]; 
let tuple2: [name: string, age: number, handsome?: boolean] = ["hh", 30, true]; 
let tuple3: [string, number, boolean] = ["hh", 30, true];
tuple3.push("回龙观"); 


let tuple4: readonly [string, number, boolean] = ["hh", 30, true];

我要求媳妇有车有房,满足即可(底线),有可能我媳妇还有钱,but 这个钱不能花,因为不知道有没有。

枚举类型

枚举可以看做是自带类型的对象,枚举的值为数字时会自动根据第一个的值来递增,枚举中里面是数字的时候可以反举。

enum USER_ROLE {
  USER, 
  ADMIN,
  MANAGER,
}

可以枚举,也可以反举


(function (USER_ROLE) {
  USER_ROLE[(USER_ROLE["USER"] = 0)] = "USER";
  USER_ROLE[(USER_ROLE["ADMIN"] = 1)] = "ADMIN";
  USER_ROLE[(USER_ROLE["MANAGER"] = 2)] = "MANAGER";
})(USER_ROLE || (USER_ROLE = {}));

异构枚举

既有数字,也有字符串

enum USER_ROLE {
  USER = "user",
  ADMIN = 1,
  MANAGER, 
}

常量枚举

如果不需要对象,如果只是使用值,可以直接采用常量枚举,否则用普通枚举

const enum USER_ROLE {
  USER,
  ADMIN,
  MANAGER,
}
console.log(USER_ROLE.USER); 
null 和 undefined

任何类型的子类型,如果 TSconfig 配置中 strictNullChecks 的值为 true,则不能把 null 和 undefined 赋给其他类型。

let u1: undefined = undefined;
let n1: null = null; 
let name1: number | boolean;
name1 = null;
name1 = undefined; 
void 类型

只能接受 null,undefined。void 表示的是空 (通常在函数的返回值中里来用);undefiend 也是空,所以 undefiend 可以赋值给 void。严格模式下不能将 null 赋予给 void。

function fn1() {}
function fn2() {
  return;
}
function fn3(): void {
  return undefined;
}
never 类型

任何类型的子类型,never 代表不会出现的值(这个类型不存在)。不能把其他类型赋值给 never。

function fn(): never {
  
  while (true) {}
}
let a: never = fn(); 
let b: number = a; 

never 实现完整性保护

function validate(type: never) {} 
function getResult(strOrNumOrBool: string | number | boolean) {
  if (typeof strOrNumOrBool === "string") {
    return strOrNumOrBool.split("");
  } else if (typeof strOrNumOrBool === "number") {
    return strOrNumOrBool.toFixed(2);
  }
  
  validate(strOrNumOrBool);
}

联合类型自动去除 never

let noNever: string | number | boolean | never = 1; 
object 对象类型

object表示非原始类型

let create = (obj: object) => {};
create({});
create([]);
create(function () {});

这里要注意不能使用大写的 Object 或 {} 作为类型,因为万物皆对象(涵盖了原始数据类型)。

object、Object、{} 的区别

  • object非原始类型;
  • Object所有值都可以赋予给这个包装类型;大 Object 是类
  • {}字面量对象类型;
Symbol 类型

Symbol 表示独一无二

const s1 = Symbol("key");
const s2 = Symbol("key");
console.log(s1 == s2); 
BigInt 类型
const num1 = Number.MAX_SAFE_INTEGER + 1;
const num2 = Number.MAX_SAFE_INTEGER + 2;
console.log(num1 == num2); 

let max: bigint = BigInt(Number.MAX_SAFE_INTEGER);
console.log(max + BigInt(1) === max + BigInt(2));

number类型和 bigInt 类型是不兼容的

any 类型

不进行类型检测,一旦写了 any 之后任何的校验都会失效。声明变量没有赋值时默认为 any 类型,写多了 any 就变成 AnyScript 了,当然有些场景下 any 是必要的。

let arr: any = ["hh", true];
arr = "回龙观";

可以在 any 类型的变量上任意地进行操作,包括赋值、访问、方法调用等等,当然出了问题就要自己负责了。

变量类型推断

TypeScript 的类型推断是根据变量的初始化值来进行推断的。如果声明变量没有赋予值时默认变量是 any 类型。

let name; 
name = "hswen";
name = 30;

声明变量赋值时则以赋值类型为准

let name = "hswen"; 
name = 30;

联合类型

在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性。

let name: string | number; 
console.log(name.toString()); 
name = 30;
console.log(name.toFixed(2)); 
name = "hswen";
console.log(name.toLowerCase()); 
字面量联合类型

type Direction = "Up" | "Down" | "Left" | "Right";
let direction: Direction = "Down";

可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。

对象的联合类型
type women =
  | {
      wealthy: true;
      waste: string;
    }
  | {
      wealthy: false;
      morality: string;
    };

let richWoman: women = {
  wealthy: true,
  waste: "不停的购物",
  morality: "勤俭持家", 
};

可以实现对象中的属性互斥。

类型断言

将变量的已有类型更改为新指定的类型,默认只能断言成包含的某个类型。

  • 非空断言

    let ele: HTMLElement | null = document.getElementById("#app");
    console.log(ele?.style.color); 
    ele!.style.color = "red"; 
    
    • 可选链操作符 ?. 在访问对象的属性或方法时,先检查目标对象及其属性是否存在。
    • 空值合并操作符 ??,当左侧的表达式结果为 nullundefined 时,会返回右侧的值。
  • 类型断言

    let name: string | number;
    (name! as number).toFixed(2); 
    (number>name!).toFixed(2);
    
    name as boolean; 
    

    尽量使用第一种类型断言因为在 React 中第二种方式会被认为是 jsx 语法

  • 双重断言

    let name: string | boolean;
    name! as any as string;
    

    尽量不要使用双重断言,会破坏原有类型关系,断言为 any 是因为 any 类型可以被赋值给其他类型。

函数类型

函数的类型就是描述了 函数入参类型与函数返回值类型

函数的两种声明方式
  • 通过 function 关键字来进行声明

    function sum(a: string, b: string): string {
      return a + b;
    }
    sum("a", "b");
    

可以用来限制函数的参数和返回值类型

  • 通过表达式方式声明

    type Sum = (a1: string, b1: string) => string;
    let sum: Sum = (a: string, b: string) => {
      return a + b;
    };
    
可选参数
let sum = (a: string, b?: string): string => {
  return a + b || "";
};
let sum = (a: string, b: string = "b"): string => {
  return a + b;
};
sum("a");

可选参数必须在其他参数的最后面。

剩余参数
const sum = (...rest: string[]): string => {
  return rest.reduce((memo, current) => (memo += current), "");
};
sum("a", "b", "c", "d");
this 类型

this 类型要进行声明

type IThis = typeof obj;
function getName(this: IThis, key: keyof IThis) {
  return this[key];
}
const obj = { name: "hh" };
getName.call(obj, "name");
  • typeof 获取对应的类型
  • keyof 获取类型对应的所有 key 类型

函数的重载

重载一般是有限的操作

重载,指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法。TypeScript 中的重载是伪重载,只有一个具体实现,是类型的重载,而不是逻辑的重载

function toArray(value: number): number[];
function toArray(value: string): string[];
function toArray(value: number | string) {
  if (typeof value == "string") {
    return value.split("");
  } else {
    return value
      .toString()
      .split("")
      .map((item) => Number(item));
  }
}
toArray(123); 
toArray("123");

重载适合于已知有限数量类型的情况,可以对不同类型的参数做出不同的处理。

类由三部分组成:构造函数、属性(实例属性、原型属性)、方法(实例方法、原型方法、访问器)

TS 中定义类
class Circle {
  x!: number; 
  y!: number;
  constructor(x: number, y: number = 0, ...args: number[]) {
    this.x = x;
    this.y = y;
  }
}
let p = new Circle(100);

实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数。

类中的修饰符
  • public修饰符(谁都可以访问到)

    class Animal {
      public name!: string; 
      public age!: number;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    }
    class Cat extends Animal {
      constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name, this.age);
      }
    }
    let p = new Cat("Tom", 18);
    console.log(p.name, p.age); 
    

    我们可以通过参数属性来简化父类中的代码。

    class Animal {
      constructor(public name: string, public age: number) {
        this.name = name;
        this.age = age;
      }
    }
    
  • protected修饰符 (自己和子类可以访问到)

    class Animal {
      constructor(protected name: string, protected age: number) {
        this.name = name;
        this.age = age;
      }
    }
    class Cat extends Animal {
      constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name, this.age);
      }
    }
    let p = new Cat("Tom", 18);
    console.log(p.name, p.age); 
    
  • private修饰符(除了自己都访问不到)

    class Animal {
      constructor(private name: string, private age: number) {
        this.name = name;
        this.age = age;
      }
    }
    class Cat extends Animal {
      constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name, this.age); 
      }
    }
    let p = new Cat("Tom", 18);
    console.log(p.name, p.age); 
    console.log(tom['name']) 
    
  • readonly修饰符(仅读修饰符)

    reaonly 在构造函数中可以随意修改(初始化)在其他的地方就不能再次修改了。

    class Animal {
      constructor(public readonly name: string, public age: number) {
        this.name = "init";
        this.age = age;
      }
      changeName(name: string) {
        this.name = name; 
      }
    }
    class Cat extends Animal {
      constructor(name: string, age: number) {
        super(name, age);
      }
    }
    let p = new Cat("Tom", 18);
    p.changeName("Jerry");
    
静态属性和方法
class Animal {
  static type = "哺乳动物"; 
  static getName() {
    
    return "动物类";
  }
  private _name: string = "Tom";
  get name() {
    
    return this._name;
  }
  set name(name: string) {
    this._name = name;
  }
}
let animal = new Animal();
console.log(animal.name);

静态属性和静态方法是可以被子类所继承的。

Super 属性
class Animal {
  say(message: string) {
    console.log(message);
  }
  static getType() {
    return "动物";
  }
}
class Cat extends Animal {
  say() {
    
    super.say("猫猫叫");
  }
  static getType() {
    
    return super.getType();
  }
}
let cat = new Cat();
console.log(Cat.getType());

这里要注意子类重写父类的方法,类型需要兼容。

class Animal {
  say(message: string): void {
    
    console.log(message);
  }
}
class Cat extends Animal {
  say(message: string) {
    super.say(message);
  }
}
let cat = new Cat();
cat.say("我要吃鱼");
私有构造函数
class Singleton {
  private static instance = new Singleton();
  private constructor() {
    
  }
  public static getInstance() {
    return Singleton.instance;
  }
}
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 == instance2);
抽象类

抽象类描述了一个类中应当有哪些成员(属性、方法等),如果在父类中定义了抽象方法,那么子类必须要实现。

  • 抽象类中不能声明静态的抽象成员
  • 抽象类中可以包含具体的实现
  • 抽象类不能被new
  • 抽象类中可以创建抽象属性和方法,让子类来实现,但是静态方法、属性不可以
abstract class Animal {
  

  
  abstract eat: () => void; 
  abstract play(): void; 
  
  drink() {
    return "喝水";
  }
}
class Tom extends Animal {
  eat!: () => void;
  play() {}
}
重载
class ToArrayConverter {
  convert(value: number): number[];
  convert(value: string): string[];
  convert(value: number | string): number[] | string[] {
    if (typeof value === "string") {
      return value.split("");
    } else {
      return value
        .toString()
        .split("")
        .map((item) => Number(item));
    }
  }
}
const converter = new ToArrayConverter();
const result1: number[] = converter.convert(123);
const result2: string[] = converter.convert("123");

TS 中类型的使用

函数类型

函数的类型就是描述了 函数入参类型与函数返回值类型

函数的两种声明方式

  • 通过 function 关键字来进行声明

    function sum(a: string, b: string): string {return a + b;}
    sum("a", "b");
    

可以用来限制函数的参数和返回值类型

  • 通过表达式方式声明

    type Sum = (a1: string, b1: string) => string;
    let sum: Sum = (a: string, b: string) => {return a + b;};
    

类型推断

TypeScript 拥有类型推导能力,根据用户的输入自动推导其类型。

赋值推断

赋值时推断,类型从右像左流动, 会根据赋值推断出变量类型

let name = "hswen"; // string
let age = 30; // number
let handsome = true; // boolean
返回值推断

自动推断函数返回值类型

function sum(a: string, b: string) {return a + b;}
sum("a", "b"); // string
上下文类型

基于位置的类型推导,反方向的类型推导

函数从左到右进行推断

type Sum = (x: string, y: number) => string;
const sum: Sum = (a, b) => a + b; // a=> string  b=> number
let result = sum("hswen", 30); // result=> string
type ICallback = (a: string, b: number, c: boolean) => void;
function fn(callback: ICallback) {let result = callback("1", 1, true); // result -> void
}

// d 类型无法正确推断,因为上下文类型是基于位置推断的
// 这里的 void 表示不关心具体类型
fn((a, b, c, d) => 100);

这里再次强调为什么 void 代表不关心?为什么这样设计呢?

[1, 2, 3].forEach((item) => item); // forEach 回调没有返回值,但是用户确可以随意返回内容,you known?

接口

接口可以在面向对象编程中表示行为的抽象,也可以描述对象的形状。接口 的作用就是为这些类型命名和为你的代码或第三方代码定义契约。(接口中不能含有具体的实现逻辑)

  • 用来描述数据形状的(对象、类、函数、混合类型)
  • 接口中的内容都是抽象的(不能有具体的实现)
函数接口参数

我们可以约束函数中的参数,但是类型无法复用。

const fullName = ({
  firstName,
  lastName,
}: {
  firstName: string;
  lastName: string;
}): string => {
  return firstName + lastName;
};

我们可以通过接口进行描述

interface IFullName {
  firstName: string;
  lastName: string;
}
const fullName = ({ firstName, lastName }: IFullName): string => {
  return firstName + lastName;
};
函数类型接口
interface IFullName {
  firstName: string;
  lastName: string;
}
interface IFn {
  (obj: IFullName): string;
}
const fullName: IFn = ({ firstName, lastName }) => {
  return firstName + lastName;
};

通过接口限制函数的参数类型和返回值类型。

type 与 interface 区别

一般场景下我们使用 interface 用来描述对象、类的结构。

使用类型别名来描述函数签名、联合类型、工具类型、映射类型。

  • type 可以用联合类型 type xx = string | number,interface 不能用联合类型
  • type 别名不能被扩展(继承),interface 可以被继承和实现
  • type 不能重名,interface 可以重名(会合并)
  • type 可以做循环和条件,interface 不行
  • 函数类型一般采用 type 声明

其它场景下,两者可以替换使用,无伤大雅。

函数混合类型
interface ICounter {
  (): number; 
  count: 0; 
}
const fn: ICounter = () => {
  
  return fn.count++;
};
fn.count = 0;
let counter: ICounter = fn;
console.log(counter());
console.log(counter());
对象接口

对象接口可以用来描述对象的形状结构

interface IVegetables {
  
  color: string;
  taste: string;
  size: number;
}
let veg1: IVegetables = {
  
  color: "red",
  taste: "sweet",
  size: 10,
  a: 1, 
};
  • 方案 1:直接采用断言的方式指定为当前赋值的类型
  • 方案 2:在类型中通过 ? 增添 a 属性为可选属性
  • 方案 3:利用同名接口合并的特点
  • 方案 4:通过接口继承的方式扩展属性
  • 方案 5:通过任意接口来扩展
  • 类型兼容性、交叉类型等

?标识的属性为可选属性, readOnly标识的属性则不能修改。多个同名的接口会自动合并

interface IVegetables {
  readonly color: string;
  size: string;
  taste: "sour" | "sweet";
}
interface IVegetables {
  a?: number;
}
const tomato: IVegetables = {
  color: "red",
  size: "10",
  taste: "sour",
};
tomato.color = "green"; 
任意属性、可索引接口
interface Person {
  name: string;
  [key: string]: any; 
}
let p: Person = {
  name: "hswen",
  age: 30,
  [Symbol()]: "回龙观",
};

任意属性可以对某一部分必填属性做限制,其余的可以随意增减。

interface IArr {
  [key: number]: any;
}
let p: IArr = {
  0: "1",
  1: "2",
  3: "3",
};
let arr: IArr = [1, "d", "c"];

可索引接口可以用于标识数组

索引访问操作符
interface IPerson1 {
  name: string;
  age: number;
  [key: string]: any;
}

type PropType1 = IPerson1["name"];
type PropType2 = IPerson1[string];

interface IPerson2 {
  name: string;
  age: number;
}
type PropTypeUnion = keyof IPerson2; 
type PropTypeValueUnion = IPerson2[PropTypeUnion]; 
类接口

这里先来强调一下抽象类和接口的区别, 抽象类中可以包含具体方法实现,接口中不能包含实现。

interface Speakable {
  name: string;
  speak(): void;
}

interface ChineseSpeakable {
  
  speakChinese(): void; 
}
class Speak implements Speakable, ChineseSpeakable {
  name!: string;
  speak() {}
  speakChinese() {}
}

一个类可以实现多个接口,在类中必须实现接口中的方法和属性。

接口继承
interface Speakable {
  speak(): void;
}
interface SpeakChinese extends Speakable {
  speakChinese(): void;
}
class Speak implements SpeakChinese {
  speakChinese(): void {
    throw new Error("Method not implemented.");
  }
  speak(): void {
    throw new Error("Method not implemented.");
  }
}
构造函数类型
interface Clazz {
  new (name: string): any;
}

function createClass(target: Clazz, name: string) {
  return new target(name); 
}
class Animal {
  constructor(public name: string) {
    this.name = name;
  }
}
let r = createClass(Animal, "Tom");

这里无法标识返回值类型。

interface ClazzT> {
  new (name: string): T;
}
function createClassT>(target: ClazzT>, name: string): T {
  return new target(name);
}
class Animal {
  constructor(public name: string) {
    this.name = name;
  }
}
let r = createClass(Animal, "Tom");

new() 表示当前是一个构造函数类型, 这里捎带使用了下泛型。在使用 createClass 时动态传入类型。

泛型

泛型就是在使用的时候确定类型,泛型类似于函数的参数。泛型参数的名称通常我们使用大写的 T / K / U / V / M / O …这种形式。

指定函数参数类型
const getArray = T>(times: number, val: T): T[] => {
  let result: T[] = [];
  for (let i = 0; i  times; i++) {
    result.push(val);
  }
  return result;
};
getArray(3, 3); 
function swapT, K>(tuple: [T, K]): [K, T] {
  return [tuple[1], tuple[0]];
}
console.log(swap(["hswen", 30]));
函数标注的方式
type TArray = T, K>(tuple: [T, K]) => [K, T];
const swap: TArray = T, K>(tuple: [T, K]): [K, T] => {
  return [tuple[1], tuple[0]];
};
interface IArray {
  T, K>(typle: [T, K]): [K, T];
}
const swap: IArray = T, K>(tuple: [T, K]): [K, T] => {
  return [tuple[1], tuple[0]];
};

两种标注方式均可,但是对于函数而言我们通常采用类型别名的方式。

泛型使用的位置

实现一个数组循环函数


type ICallbackT> = (item: T, index: number) => void;
type IForEach = T>(arr: T[], callback: ICallbackT>) => void;

const forEach: IForEach = (arr, callback) => {
  for (let i = 0; i  arr.length; i++) {
    callback(arr[i], i); 
  }
};
forEach([1, 2, "a", "b"], function (item) {
  console.log(item);
});

泛型 T 写在前面就是表示使用类型的时候传参,写到函数的前面意味着着调用函数的时候传递参数

默认泛型

在使用一些联合类型的时候,会使用泛型

type Union = number | T;
const u1: Union = "abc";
const u2: Union = true;

可以指定泛型的默认类型,让使用更方便。

泛型约束

使用 extends 关键字来约束传入的泛型参数必须符合要求。A extends B 意味着 A 是 B 的子类型

  • 'abc' extends string
  • 'a' extends 'a' | 'b'

案例 1:

function handleT extends string | number>(input: T): T {
  return input;
}

案例 2:

interface IWithLength {
  length: number;
}
function getLenT extends IWithLength>(val: T) {
  return val.length;
}
getLen("hello");

案例 3:

const getVal = T, K extends keyof T>(obj: T, key: K): T[K] => {
  return obj[key];
};
getVal({ name: "hh" }, "name");

泛型约束经常也配合着条件类型来使用,后面讲到条件类型时在详细说明。

对象中的泛型

通过接口定义一个特定的响应类型结构

interface ApiResponseT = any> {
  code: number;
  data: T;
  message?: string;
}

调用接口时传入返回数据的结构类型

通泛型坑位,来占位置

interface LoginRes {
  
  token: string;
  roles: number[];
}

function toLogin(): ApiResponseLoginRes> {
  return {
    code: 0,
    data: {
      token: "Bear token",
      roles: [1, 2],
    },
  };
}
类中的泛型

创建实例时提供类型

class MyArrayT> {
  
  arr: T[] = [];
  add(num: T) {
    this.arr.push(num);
  }
  getMaxNum(): T {
    let arr = this.arr;
    let max = arr[0];
    for (let i = 1; i  arr.length; i++) {
      let current = arr[i];
      current > max ? (max = current) : null;
    }
    return max;
  }
}
let myArr = new MyArraynumber>(); 
myArr.add(3);
myArr.add(1);
myArr.add(2);
console.log(myArr.getMaxNum());

交叉类型

交叉类型 (Intersection Types) 是将多个类型合并为一个类型

  • 联合类型的符号是 |,类似按位或。只需要符合联合类型中的一个类型即可。(并集)
  • 交叉类型的符号是 &,类似按位与。需同时满足类型。(交集)
interface Person1 {
  handsome: string;
}
interface Person2 {
  high: string;
}
type P1P2 = Person1 & Person2;
let p: P1P2 = { handsome: "帅", high: "高" };

举例:我们提供两拨人,一拨人都很帅、另一拨人很高。我们希望找到他们的交叉部分 => 又高又帅的人。

interface Person1 {
  handsome: string;
  address: {
    pos: string;
  };
}
interface Person2 {
  high: string;
  address: {
    pos: number;
  };
}
type P1P2 = Person1 & Person2; 
type POS = P1P2["address"]["pos"]; 
function mixinT, K>(a: T, b: K) {
  return { ...a, ...b };
}
const x = mixin({ name: "hs", age: 30 }, { age: "20" });

这里返回值默认会被识别成交叉类型,但是如果两个对象中有相同属性类型不同,则默认推导会出现问题,后续我们再来解决这个问题。

unknown

unknown类型,任何类型都可以赋值为 unknown 类型。它是 any 类型对应的安全类型。any 叫做不检测了,unknown 要进行类型检测

不能访问 unknown 类型上的属性,不能作为函数、类来使用


function processInput(input: unknown) {
  if (typeof input === "string") {
    console.log(input.toUpperCase());
  } else if (typeof input === "number") {
    console.log(input.toFixed(2));
  } else {
    console.log(input); 
  }
}

let name: unknown = "hswen";
(name as string).toUpperCase();

使用 unknown 类型需要进行类型检查或类型断言后再进行使用。

unknown 特性

  • 联合类型中的unknown

    type UnionUnknown = unknown | null | string | number;
    

    联合类型与 unknown 都是 unknown 类型

  • 交叉类型中的unknown

    unknown 表示类型未知,null 是一种具体的值,结果会受到 null 的限制,最终结果会变成 null 类型,而不是保持 unknown 类型。

    type inter = unknown & null; 
    type inter = any & null; 
    

    交叉类型与 unknown 都是其他类型

  • keyof unknown 是 never

    type key = keyof unknown; 
    
    

条件类型

条件类型的语法类似于三元表达式

条件类型基本使用

可以使用 extends 关键字和三元表达式,实现条件判断。条件类型大部分场景是和泛型一起使用的

type ResStatusMessageS extends number> = S extends 200 | 201 | 204
  ? "success"
  : "fail";
type Message = ResStatusMessage300>; 
type ConditionalT, C> = T extends C ? true : false;
type R1 = Conditional"hswen", string>; 
type R2 = Conditional"hswen", number>; 
interface Fish {
  name: "鱼";
}
interface Water {
  type: "水";
}
interface Bird {
  name: "鸟";
}
interface Sky {
  type: "天空";
}
type ConditionT> = T extends Fish ? Water : Sky; 
let con1: ConditionFish> = { type: "水" };
多条件类
type FormatReturnTypeT> = T extends string 
  ? string
  : T extends number
  ? number
  : never;

function sumT extends string | number>(x: T, y: T): FormatReturnTypeT> {
  
  return x + (y as any);
}
sum("abc", "abc"); 
sum(123, 123); 

类型兼容性问题

extends 本质上是判断类型的兼容性,只需要兼容则条件即可成立

基本数据类型的兼容性
type R1 = "abc" extends string ? true : false; 
type R2 = 123 extends number ? true : false; 
type R3 = true extends boolean ? true : false; 


let r1: string = "abc";
let r2: number = 123;
let r3: boolean = true;

字面量类型可以赋予给原始数据类型。

联合类型的兼容性

在联合类型中,只需要符合其中一个类型即是兼容,从安全角度来看,就是你赋值的类型我这里支持。

type R4 = "a" extends "a" | "b" | "c" ? true : false; 
type R5 = 123 extends 123 | 456 | 789 ? true : false; 
type R6 = string extends boolean | string | number ? true : false;


let r4: "a" | "b" | "c" = "a";
let r5: 123 | 456 | 789 = 123;
let temp = "hello";
let r6: boolean | string | number = temp;

联合类型中所有成员在另一个联合类型中都能找到就是兼容

原始类型与装箱类型兼容性

大写的就是装箱类型

type R7 = string extends String ? true : false; 
type R8 = number extends Number ? true : false; 
type R9 = object extends Object ? true : false; 
type R10 = String extends Object ? true : false; 


let r7: String = "abc";
let r8: Number = 123;
let r9: Object = {};
let r10: Object = new String("abc");

原始类型可以赋予给装箱类型最终可以赋予给 Object 类型。

any 及 unknown
type R11 = Object extends any ? true : false; 
type R12 = Object extends unknown ? true : false; 


let tempObj: Object = {};
let r11: any = tempObj;
let r12: unknown = tempObj;

any 和 unkown 即为顶级类型。

其它类型的兼容性
  • never 是任何类型的子类型,也就是最底端的类型
  • null 和 undefiend 在严格模式下不能赋予给其他类型。undefined 可以赋予给 void 类型
type R13 = never extends "abc" ? true : false; 
type R14 = undefined extends undefined ? true : false; 
type R15 = null extends null ? true : false; 
type R16 = undefined extends void ? true : false; 

never 为最底端类型。

Never《字面量《字面量联合类 型 | 字面量类型《原始数据类型《包装类型《Object

类型层级

根据类型兼容性我们可以得出以下结论:

  • never
  • 字面量类型
  • 原始类型
  • 原始类型
  • Object
unknown & any 特殊情况
type R17 = unknown extends 1 ? true : false; 
type R18 = any extends 1 ? true : false; 
type R19 = any extends any ? true : false; 

any可以分解成条件满足、和不满足两部分,则 返回条件类型结果组成的联合类型。但是与any 进行判断时依然会进行正常判断。

{} | object | Object 特殊情况
type R20 = {} extends object ? true : false; 
type R21 = {} extends Object ? true : false; 


type R22 = Object extends {} ? true : false; 
type R23 = object extends {} ? true : false; 


type R24 = Object extends object ? true : false; 
type R25 = object extends Object ? true : false; 

条件类型与映射类型

条件类型分发

出现条件分发的场景
  • 类型参数需要是一个联合类型。
  • 类型参数需要通过泛型参数的方式传入
  • 条件类型中的泛型参数是否完全裸露,只有裸类型才可以被分发。
type Condition1 = Fish | Bird extends Fish ? Water : Sky; 
type Condition2T> = T extends Fish ? Water : Sky;
type R1 = Condition2Fish | Bird>; 

这里会用每一项依次进行分发, 最终采用联合类型作为结果, 等价于:

type c1 = Condition2Fish>;
type c2 = Condition2Bird>;
type c = c1 | c2;
禁用分发

默认情况下有些时候我们需要关闭这种分发能力,会造成判断不准确


type R1 = unionAssets1 | 2, 1 | 2 | 3>; 
type R2 = unionAssets1 | 2, 1>; 


type unionAssetsT, U> = [T] extends [U] ? true : false;
type NoDistributeT> = T & {}; 
type unionAssetsT, U> = NoDistributeT> extends U ? true : false;
特殊问题

通过泛型传入的参数为 never,则会直接返回 never。

type isNever1T> = T extends never ? true : false;
type isNever2T> = [T] extends [never] ? true : false; 
type R4 = isNever1never>; 
type R5 = isNever2never>; 

内置条件类型

Extract抽取类型(交集)
type ExtractT, U> = T extends U ? T : never;
type MyExtract = Extract"1" | "2" | "3", "1" | "2">;
Exclude排除类型(差集)
type ExcludeT, U> = T extends U ? never : T;
type MyExclude = Exclude"1" | "2" | "3", "1" | "2">;

补集如何实现呢?约束 U 是 T 的子集求出来的就是补集了。

type ComplementT, U extends T> = T extends U ? never : T;
type MyComplement = Complement"1" | "2" | "3", "1" | "2">; 
NoNullable 非空检测
type NonNullableT> = T extends null | undefined ? never : T;
type NonNullableT> = T & {}; 
type MyNone = NonNullable"a" | null | undefined>;

infer 类型推断

TypeScript 中通过 infer(inference)关键字在条件类型中提取类型的某一部分信息。根据 infer 的位置不同,我们就能够获取到不同位置的类型。

基于 infer 的内置类型

使用 infer 需要先创造一个条件才可以

  • ReturnType 返回值类型

    function getUser(a: number, b: number) {
      return { name: "hswen", age: 30 };
    }
    type ReturnTypeT> = T extends (...args: any) => infer R ? R : never;
    type MyReturn = ReturnTypetypeof getUser>;
    
  • Parameters 参数类型

    type ParametersT> = T extends (...args: infer R) => any ? R : any;
    type MyParams = Parameterstypeof getUser>;
    
  • ConstructorParameters 构造函数参数类型

    class Person {
      constructor(name: string, age: number) {}
    }
    type ConstructorParametersT> = T extends { new (...args: infer R): any }
      ? R
      : never;
    type MyConstructor = ConstructorParameterstypeof Person>;
    
  • InstanceType 实例类型

    type InstanceTypeT> = T extends { new (...args: any): infer R } ? R : any;
    type MyInstance = InstanceTypetypeof Person>;
    
内置类型的使用
function createInstanceT extends new (...args: any[]) => any>(
  Ctor: T,
  ...args: ConstructorParametersT>
): InstanceTypeT> {
  return new Ctor(...args);
}
class Animal {
  constructor(public name: string) {}
}
const animal = createInstance(Animal, "动物");
infer 实践

类型交换

type SwapT> = T extends [infer A, infer B] ? [B, A] : T;
type SwapS1 = Swap["hh", 30]>; 
type SwapS2 = Swap[1, 2, 3]>; 
type TailToHeadT> = T extends [infer A, ...infer Args, infer B]
  ? [B, A, ...Args]
  : T;
type R100 = TailToHead["hh", 30, "回龙观"]>; 

递归推断

type PromiseValT> = T extends Promiseinfer V> ? PromiseValV> : T;
type PromiseResult = PromiseValPromisePromisenumber>>>; 

将数组类型转化为联合类型

type ElementOfT> = T extends Arrayinfer E> ? E : never;
type TupleToUnion = ElementOf[string, number, boolean]>;
type TupleToUnion = [string, number, boolean][number];

映射类型

所谓的映射类型,类似于 map 方法。核心就是基于键名映射到键值类型(使用的是 in 关键字)

type A1 = { name: string };
type A2 = { age: number };

type ComputeT> = {
  
  [K in keyof T]: T[K];
};
type A1A2 = ComputeA1 & A2>; 
Partial 转化可选属性
interface Company {
  num: number;
}
interface Person {
  name: string;
  age: string;
  company: Company;
}

type PartialPerson = PartialPerson>;

遍历所有的属性将属性设置为可选属性, 但是无法实现深度转化!

type DeepPartialT> = {
  [K in keyof T]?: T[K] extends object ? DeepPartialT[K]> : T[K];
};
type DeepPartialPerson = DeepPartialPerson>;

我们可以实现深度转化, 如果值是对象继续深度转化。

Required
type PartialPerson = PartialPerson>;
type RequiredT> = { [K in keyof T]-?: T[K] };
type RequiredPerson = RequiredPartialPerson>;

将所有的属性转化成必填属性

Readonly 转化仅读属性
type ReadonlyT> = { readonly [K in keyof T]: T[K] };
type ReadonlyPerson = ReadonlyPartialPerson>;

将所有属性变为仅读状态。

type MutableT> = { -readonly [K in keyof T]: T[K] }; 
type MutablePerson = MutableReadonlyPerson>;

结构类型

Pick 挑选所需的属性
type PickT, U extends keyof T> = { [P in U]: T[P] };
type PickPerson = PickPerson, "name" | "age">;

在已有类型中挑选所需属性。

Omit 忽略属性
let person = {
  name: "hs",
  age: 11,
  address: "回龙观",
};
type OmitT, K extends keyof T> = PickT, Excludekeyof T, K>>;
type OmitAddress = Omittypeof person, "address">;

忽略 person 中的 address 属性 (先排除掉不需要的 key,在通过 key 选出需要的属性)

function mixinT, K>(a: T, b: K): OmitT, keyof K> & K {
  return { ...a, ...b };
}
const x = mixin({ name: "hs", age: 30 }, { age: "20" });
Record 记录类型

只想要 key-> value 的格式可以采用 Record 类型

record 通常用来代替 object。

type RecordK extends keyof any, T> = { [P in K]: T };
let person: Recordstring, any> = { name: "hswen", age: 30 };

实现 map 方法,我们经常用 record 类型表示映射类型

function mapT extends keyof any, K, U>(
  obj: RecordT, K>,
  callback: (item: K, key: T) => U
) {
  let result = {} as RecordT, U>;
  for (let key in obj) {
    result[key] = callback(obj[key], key);
  }
  return result;
}
const r = map({ name: "hswen", age: 30 }, (item, key) => {
  return item;
});

兼容性

TypeScript 的类型系统特性:结构化类型系统(鸭子类型检测),TypeScript 比较两个类型不是通过类型的名称,而是比较这两个类型上的属性与方法

基本数据类型的兼容性

你要的我有就可以

let obj: {
  toString(): string;
};
let str: string = "hh";
obj = str; 

string 可以看成基于对象 toString 进行扩展的子集,(从安全度考虑,因为在最后使用 obj 时只允许调用 toString 方法)

接口兼容性
interface IAnimal {
  name: string;
  age: number;
}
interface IPerson {
  name: string;
  age: number;
  address: string;
}
let animal: IAnimal;
let person: IPerson = {
  name: "hh",
  age: 30,
  address: "回龙观",
};
animal = person;

接口的兼容性,只要满足接口中所需要的类型即可!

函数的兼容性

函数的兼容性主要是比较参数和返回值

  • 参数

    let sum1 = (a: string, b: string) => a + b;
    let sum2 = (a: string) => a;
    sum1 = sum2;
    

    赋值函数的参数要少于等于被赋值的函数,与对象相反, 例如:

    type FuncT> = (item: T, index: number) => void;
    function forEachT>(arr: T[], cb: FuncT>) {
      for (let i = 0; i  arr.length; i++) {
        cb(arr[i], i);
      }
    }
    forEach([1, 2, 3], (item) => {
      console.log(item);
    });
    
  • 返回值

    type sum1 = () => string | number;
    type sum2 = () => string;
    
    let fn1: sum1;
    let fn2!: sum2;
    fn1 = fn2;
    
类的兼容性
class ClassA {
  name: string = "hh";
  age: number = 30;
}
class ClassB {
  name: string = "hh";
  age: number = 30;
  address: string = "回龙观";
}
let parent: ClassA = new ClassB(); 

这里要注意的是,只要有 private 或者 protected 关键字类型就会不一致

class ClassA {
  private name: string = "hh";
  age: number = 30;
}
class ClassB {
  private name: string = "hh";
  age: number = 30;
}
let clazz: ClassA = new ClassB(); 

结构化类型导致的问题

type BTC = number; 
type USDT = number;

let btc: BTC = 1000;
let usdt: USDT = 1000;

function getCount(count: BTC) {
  return count as BTC;
}
let count = getCount(usdt); 
type NominalT, U extends string> = T & { __tag: U };
type BTC = Nominalnumber, "btc">;
type USDT = Nominalnumber, "usdt">; 

let btc: BTC = 1000 as BTC;
let usdt: USDT = 1000 as USDT;
function getCount(count: BTC) {
  
  return count;
}
let count = getCount(usdt); 
函数的逆变与协变

函数的参数是逆变的,返回值是协变的(在非严格模式下 StrictFunctionTypes:false 函数的参数是双向协变的)

class Parent {
  house() {}
}
class Child extends Parent {
  car() {}
}
class Grandson extends Child {
  sleep() {}
}
function fn(callback: (instance: Child) => Child) {
  
  callback(new Child());
  let ins = callback(new Grandson()); 
  
}
fn((instance: Parent) => {
  
  
  return new Grandson();
});

通过这个案例可以说明,函数签名类型中参数是逆变的,返回值可以返回子类型所以称之为协变的。

随着某一个量的变化而变化一致的即称为协变,而变化相反的即称为逆变。但是参数逆变也会带来一些问题。

传递的函数(传父(参数是逆变的)返子(返回值是协变的))

由此可得:

type ArgT> = (arg: T) => void;
type ReturnT> = (arg: any) => T;
type ArgReturn = ArgParent> extends ArgChild> ? true : false; 
type ReturnReturn = ReturnGrandson> extends ReturnChild> ? true : false; 

逆变带来的问题:

interface ArrayT> {
  
  concat(...args: T[]): T[]; 
  [key: number]: T;
}
let parentArr!: ArrayParent>;
let childArr!: ArrayChild>;

parentArr = childArr; 
泛型的兼容性
interface ITT> {}
let obj1: ITstring>;
let obj2!: ITnumber>;
obj1 = obj2;
枚举的兼容性
enum USER1 {
  role = 1,
}
enum USER2 {
  role = 1,
}
let user1!: USER1;
let user2!: USER2;
user1 = user2; 

不同的枚举类型不兼容。

类型保护

通过判断、识别所执行的代码块,自动识别变量属性和方法。将类型范围缩小。

typeof 类型保护
function double(val: number | string) {if (typeof val === "number") {val.toFixed();
  } else {val.charAt(0);
  }
}
instanceof 类型保护
class Cat {}
class Dog {}

const getInstance = (clazz: { new (): Cat | Dog }) => {return new clazz();
};
let r = getInstance(Cat);
if (r instanceof Cat) {r;} else {r;}
in 类型保护
interface Fish {swiming: string;}
interface Bird {
  fly: string;
  leg: number;
}
function getType(animal: Fish | Bird) {if ("swiming" in animal) {animal; // Fish} else {animal; // Bird}
}
可辨识联合类型
interface WarningButton {class: "warning";}
interface DangerButton {class: "danger";}
function createButton(button: WarningButton | DangerButton) {if (button.class == "warning") {button; // WarningButton} else {button; // DangerButton}
}
// ----------- 类型中有独一无二的特性 ---------------
function ensureArray(input: T | T[]): T[] {return Array.isArray(input) ? input : [input];
}
null 保护
const addPrefix = (num?: number) => {
  num = num || 1.1;
  function prefix(fix: string) {return fix + num?.toFixed();
  }
  return prefix("$");
};
console.log(addPrefix());

这里要注意的是 ts 无法检测内部函数变量类型。

自定义类型保护
interface Fish {swiming: string;}
interface Bird {
  fly: string;
  leg: number;
}
function isBird(animal: Fish | Bird): animal is Bird {return "swiming" in animal;}
function getAniaml(animal: Fish | Bird) {if (isBird(animal)) {animal;} else {animal;}
}

自定义类型

内置类型可以分为以下几种类别:

  • Partial、Required、Readonly 起到修饰的作用
  • Pick Omit 处理数据结构
  • Exclude、Extract 处理集合类型
  • Parameters ReturnType 等 模式匹配类型
部分属性可选(修饰类型)
// 解题思路:将对应的属性挑选出来变为可选项 + 忽略掉对应的属性
type PartialPropsOptional = Partial
> &
  Omit;

interface Person {
  name: string;
  age: number;
  address: string;
}
type Compute = {[K in keyof T]: T[K];
};
type t1 = Compute>;
根据值类型(挑选 / 忽略)对象类型的属性(结构类型)
// 解题思路:先找出类型相等的 key,在通过 Pick/Omit 进行筛选

// 1)判断两个类型是否相等
type IsEqual = [T] extends [U]
  ? [U] extends [T]
    ? Success
    : Fail
  : Fail;

// 2) 如果相等,则返回对应的 key。再取其联合类型
type ExtractKeysByValueType = {[K in keyof T]: IsEqual;
}[keyof T];

// 3)通过联合类型挑选出所需的类型
type PickKeysByValue = Pick
>;

type t2 = PickKeysByValue; // {name:string,address:string}
// 在来实现 Omit:编写 Omit 逻辑应到正好相反
type ExtractKeysByValueType = {[K in keyof T]: IsEqual, //  是 Omit 则为 never
    IsEqual //  不是 Omit 就返回 key
  >;
}[keyof T];
type OmitKeysByValue = Pick // 增加类型来判断是否是 Omit
>;

type t3 = OmitKeysByValue;
// 重映射实现
type PickKeysByValue = {[K in keyof T as T[K] extends U ? K : never]: T[K];
};
子类型互斥(集合类型)
interface Man1 {fortune: string;}
interface Man2 {funny: string;}
interface Man3 {foreign: string;}
// type ManType = Man1 | Man2 | Man3; // 我希望 MainType 只能是其中的一种类型
// let man: ManType = {
//   fortune: "富有",
//   funny: "风趣",
//   foreign: "洋派",
// };
// 1)将对象的差集标记为 never
type DiscardType = {[K in Exclude]?: never };

// 2) 差集(never)+ 另一半
// (man1 - man2) 这里的属性标记为 never + man2
// (man2 - man1) 这里的属性标记为 never + man1
type OrType = (DiscardType & U) | (DiscardType & T);
// type ManType = OrType;
type ManType = OrType>;
对象的交、差、并、补 (集合类型)
type A = {
  name: string;
  age: number;
  address: string;
};

type B = {
  name: string;
  male: boolean;
  address: number;
};

交集

type ObjectInter = Pick
>;

差集

type ObjectDiff = Pick
>;

补集

// T 多 U 少
type ObjectComp = Pick
>;

重写

以后面的类型为准(取交集)在加上以前比现在多的类型。

// 取出覆盖的类型 + 加上差集
type Overwrite = ObjectInter &
  ObjectDiff;
模式匹配类型
// 推断函数类型中参数的最后一个参数类型
type LastParameter any> = T extends (...arg: infer P) => any
  ? P extends [...any, infer L]
    ? L
    : never
  : never;

借助 Parameters 类型简化

type LastParameter any> = Parameters extends [
  ...any,
  infer Q
]
  ? Q
  : never;

模块及命名空间使用

模块和命名空间

默认情况下 , 我们编写的代码处于全局命名空间中

模块

文件模块:如果在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域。


export default "hh";


import name from "./a";

ESM 可以打包成 Commonjs 规范以及 AMD 规范,但是 commonjs 规范无法打包成 AMD 规范。

如果一个模块是用 commonjs 规范来编写的,那么也无法采用 ES 模块方式来导入

TS 模块语法


export = "hh";


import name = require("./a"); 
命名空间

命名空间可以用于组织代码,避免文件内命名冲突(内部模块)。想要被外界使用也可以通过 export 导出命名空间。


export namespace Zoo {
  export class Dog {
    eat() {
      console.log("zoo dog");
    }
  }
}
export namespace Home {
  export class Dog {
    eat() {
      console.log("home dog");
    }
  }
}

import { Zoo, Home } from "./a";
let dog_of_zoo = new Zoo.Dog();
dog_of_zoo.eat();
let dog_of_home = new Home.Dog();
dog_of_home.eat();
export namespace Earth {
  export namespace Contry {
    export class China {}
    export class America {}
  }
}
Earth.Contry.China;
Earth.Contry.America;

命名空间中导出的变量可以通过命名空间使用。

命名空间合并

同名的命名空间可以自动合并,如果命名空间散落到多个文件中想要被合并,可以采用后面要学的三斜线指令。

export namespace Zoo {
  export class Dog {
    eat() {
      console.log("zoo dog");
    }
  }
}
export namespace Zoo {
  export class Monkey {
    eat() {
      console.log("zoo monkey");
    }
  }
}

命名空间也可用于:扩展类、扩展方法、扩展枚举类型。

class A {
  static b = "hello b";
}
namespace A {
  export let a = "hello a";
}

function counter(): number {
  return counter.count++;
}
namespace counter {
  export let count = 0;
}

enum ROLE {
  user = 0,
}
namespace ROLE {
  export let admin = 1;
}

类型声明

声明全局变量

普通类型声明

declare let age: number;
declare function sum(a: string, b: string): void;
declare class Animal {}
declare const enum Seaons {
  Spring,
  Summer,
  Autumn,
  Winter,
}
declare interface Person {
  name: string;
  age: number;
}

一般情况下,我们会将 declare 声明的内容放置到类型声明文件中即 .d.ts 中,这样不会影响核心代码,并且统一管理。默认项目编译时会查找所有以 .d.ts 结尾的文件。

练习: 声明 jQuery 类型

jquery 通过外部 CDN 方式引入,想在代码中直接使用

interface JQuery {
  height(num?: number): this;
  width(num?: number): this;
  extend(obj: object): this;
}



声明模块

declare module "mitt" {
  type Type = string | symbol;
  type Listener = (...args: any[]) => void;
  const on: (type: Type, listener: Listener) => this;
  const emit: (type: Type, ...args: any[]) => boolean;
  const off: (type: Type, listener: Listener) => Listener;
}
declare module "*.jpg" {
  const str: string;
  export default str;
}


import mitt from "mitt";
import type { Listener } from "mitt"; 
import url from "a.jpg";
let listener: Listener = function (data) {
  console.log(data);
};
mitt.on("data", listener);
mitt.emit("data", "this is data");
mitt.off("data", listener);
第三方声明文件

@types 是一个约定的前缀,所有的第三方声明的类型库都会带有这样的前缀

npm install @types/jquery -S

当使用 jquery 时默认会查找 node_modules/@types/jquery/index.d.ts 文件

查找规范

  • node_modules/jquery/package.json 中的 types 字段
  • node_modules/jquery/index.d.ts
  • node_modules/@types/jquery/index.d.ts

自己编写的声明文件放到目录中@types/lodash



export = _; 
export as namespace _; 

declare namespace _ {
  function a(): void;
  function b(): void;
  function c(): void;
}

import _ = require("./lodash");
declare module "./lodash" {
  
  function x(): void;
  function y(): void;
  function z(): void;
}

namespace表示一个全局变量包含很多子属性 , 命名空间内部不需要使用 declare 声明属性或方法


export = _;
export as namespace _;
declare const _: _.ILodash; 
declare namespace _ {
  interface ILodash {
    
    a(): void;
    b(): void;
    c(): void;
  }
}
import _ = require("./lodash");
declare module "./lodash" {
  interface ILodash {
    
    x(): void;
    y(): void;
    z(): void;
  }
}
三斜线指令

三斜线指令就是声明文件中的导入语句,用于 声明当前的文件依赖的其他类型声明

三斜线指令必须被放置在文件的顶部才有效




我们一般只使用第一种方式,来进行声明的整合。

扩展全局变量类型

可以直接使用接口对已有类型进行扩展

interface String {
  double(): string;
}
String.prototype.double = function () {
  return (this as string) + this;
};
interface Window {
  mynane: string;
}
console.log(window.mynane);

模块内全局扩展

declare global {
  interface String {
    double(): string;
  }
  interface Window {
    myname: string;
  }
}

声明全局表示对全局进行扩展。

TS 注释

@ts-ignore

忽略下一行的检测,不管是否有错误。


let name: string = "30";
@ts-expect-error

下一行代码真的存在错误时才能被使用。


const age: number = 30;
ts-nocheck

忽略整个文件的类型检测


const age: number = "30";
const name: string = 30;
ts-check

用于为 JavaScript 文件进行类型检查(需要配合 JSDoc



function getType(a, b) {
  return a + b;
}
getType("1", "2");


const age = 30;

类型体操

基于字符串

CapitalizeString

首字母大写


export type CapitalizeStringT> = T extends `${infer L}${infer R}`
  ? `${CapitalizeL>}${R}` 
  : T; 



type a1 = CapitalizeString"handler">; 
type a2 = CapitalizeString"parent">; 
type a3 = CapitalizeString233>; 
FirstChar

获取字符串字面量中的第一个字符

export type FirstChar = T extends `${infer L}${infer R}` ? L : never;

// ---------------------------------

type A = FirstChar; // 'B'
type B = FirstChar; // 'd'
type C = FirstChar; // never
LastChar

获取字符串字面量中的最后一个字符

// 拆分左右两边类型,将右边递归拆分,通过泛型保留拆分后的结果
export type LastChar = T extends `${infer L}${infer R}`
  ? LastChar // 递归拆分右侧内容,L 为上一次的左侧,最后不能拆分则返回 L,L 就位最后一个字符
  : F;

// ---------------------------------

type A = LastChar; // 'E'
type B = LastChar; // 'v'
type C = LastChar; // never
StringToTuple

字符串转换为元组类型

export type StringToTuple = T extends `${infer L}${infer R}` ? StringToTuple : F;

type A = StringToTuple; // ['B', 'F', 'E', '.', 'd', 'e','v']
type B = StringToTuple; // []
TupleToString

将字符串类型的元素转换为字符串字面量类型

// 拆分左右两边类型,将右边递归拆分,通过泛型保留拆分后的结果.
export type TupleToString = T extends [
  infer L,
  ...infer R
]
  ? TupleToString // 递归数组右侧的部分,每次拿到的左侧结果累加
  : F;

// ---------------------------------
type A = TupleToString; // 'abc'
type B = TupleToString; // 'a'
type C = TupleToString; // ''
RepeatString

复制字符 T 为字符串类型,长度为 C

export type RepeatString = C extends A["length"] ? F : RepeatString;
// ---------------------------------

type A = RepeatString; // 'aaa'
type B = RepeatString; // ''
SplitString

将字符串字面量类型按照指定字符,分割为元组。无法分割则返回原字符串字面量

export type SplitString = T extends `${infer L}${S}${infer R}`
  ? SplitString // 递归拆分右边,并且将左边放到数组中
  : [...A, T]; // 不包含则直接将 T 放到数组中

// ---------------------------------

type A1 = SplitString; // ["handle", "open", "flag"]
type A2 = SplitString; // ["open", "flag"]
type A3 = SplitString; // ["handle", "open", "flag"]
type A4 = SplitString; // ["open", "flag"]
type A5 = SplitString; // ["open.flag"]
LengthOfString

计算字符串字面量类型的长度

export type LengthOfString = T extends `${infer L}${infer R}`
  ? LengthOfString
  : A["length"];

// ---------------------------------

type A = LengthOfString; // 7
type B = LengthOfString; // 0
KebabCase

驼峰命名转横杠命名

type RemoveFirst = T extends `-${infer R}` ? R : T; // 删除首字母是 - 的

export type KebabCase = T extends `${infer L}${infer R}`
  ? // 看当前字母是否是大写,如果是 则转化成 - 小写 H -> -h
    KebabCase extends L ? `-${Lowercase}` : L}`>
  : RemoveFirst;

// ---------------------------------

type a1 = KebabCase; // handle-open-flag
type a2 = KebabCase; // open-flag
CamelCase

横杠命名转化为驼峰命名

type CamelCase = T extends `${infer L}-${infer R1}${infer R2}` // 匹配 xx-x => xxX
  ? CamelCase}`> // 累加 - 左边
  : Capitalize;

// ---------------------------------

type a1 = CamelCase; // HandleOpenFlag
type a2 = CamelCase; // OpenFlag
ObjectAccessPaths

得到对象中的值访问字符串

type RemoveFirst = T extends `.${infer L}` ? L : T;
export type ObjectAccessPaths = K extends keyof T // 产生一次分发操作
  ? T[K] extends object // 不能 T[K]联合类型会出现 never
    ? ObjectAccessPaths
    : RemoveFirst
  : never;

// ---------------------------------

function createI18n(schema: Schema): (path: ObjectAccessPaths) => void {return (path) => {};}

const i18n = createI18n({
  home: {
    topBar: {
      title: "顶部标题",
      welcome: "欢迎登录",
    },
    bottomBar: {notes: "XXX 备案,归 XXX 所有",},
  },
  login: {
    username: "用户名",
    password: "密码",
  },
});

i18n("home.topBar.title"); // correct
i18n("home.topBar.welcome"); // correct
i18n("home.bottomBar.notes"); // correct

// i18n('home.login.abc')              // error,不存在的属性
// i18n('home.topBar')                 // error,没有到最后一个属性
Include

判断传入的字符串字面量类型中是否含有某个字符串

type Include = T extends ""? C extends""
    ? true
    : false
  : T extends `${infer L}${C}${infer R}` // 可以实现 startsWith、endsWith
  ? true
  : false;

// ---------------------------------

type a1 = Include; // true
type a2 = Include; // true
type a3 = Include; // true 空字符串时需要特殊处理
Trim
type TrimLeft = T extends ` ${infer R}` ? TrimLeft : T; // 去左空格
type TrimRight = T extends `${infer L} ` ? TrimLeft : T; // 去右空格
type Trim = TrimRight>;

// ---------------------------------

type a1 = Trim;
Replace
export type Replace = C extends ""? T extends""
    ? RC // 两方都是空,直接返回替换后的结果
    : `${RC}${T}` // 如果被替换值为空,则把替换的结果换到前面
  : T extends `${infer L}${C}${infer R}`
  ? Replace
  : F;

// ---------------------------------

type a1 = Replace;
type a2 = Replace;
type a3 = Replace;
type a4 = Replace;
ComponentEmitsType

定义组件的监听事件类型

import {CamelCase} from "./10.CamelCase";

// 实现 ComponentEmitsType 类型,将
type a1 = {"handle-open": (flag: boolean) => true;
  "preview-item": (data: { item: any; index: number}) => true;
  "close-item": (data: { item: any; index: number}) => true;
};

type ComponentEmitsType = {[K in keyof T as `on${CamelCase}`]?: T[K] extends (...args: infer R) => any
    ? (...args: R) => void
    : T[K];
};

type a2 = ComponentEmitsType;
// 转化为类型
/*
{onHandleOpen?: (flag: boolean) => void,
    onPreviewItem?: (data: { item: any, index: number}) => void,
    onCloseItem?: (data: { item: any, index: number}) => void,
}
*/

基于数组

LengthOfTuple

计算元组类型的长度

export type LengthOfTuple = T["length"];

// -----------------------

type A = LengthOfTuple; // 3
type B = LengthOfTuple; // 0
FirstItem

得到元组类型中的第一个元素

export type FirstItem = T[0];

// -----------------------

type A = FirstItem; // string
type B = FirstItem; // 'B'
LastItem

得到元组类型中的最后一个元素

export type LastItem = T extends [...infer L, infer R]
  ? R
  : never;

// -----------------------

type A = LastItem; // boolean
type B = LastItem; // 'E'
type C = LastItem; // never
Shift

移除元组类型中的第一个类型

export type Shift = T extends [infer L, ...infer R] ? R : [];

// -----------------------

type A = Shift; // [2,3]
type B = Shift; // []
type C = Shift; // []
Push

在元组类型 T 中添加新的类型 I

export type Push = [...T, I];

// -----------------------

type A = Push; // [1,2,3,4]
type B = Push; // [1, 2]
ReverseTuple

反转元组

export type ReverseTuple = T extends [
  infer L,
  ...infer R
]
  ? ReverseTuple
  : F;

// -----------------------

type A = ReverseTuple; // [boolean, number, string]
type B = ReverseTuple; // [3,2,1]
type C = ReverseTuple; // []
Flat

拍平元组

export type Flat = T extends [infer L, ...infer R]
  ? [...(L extends any[] ? Flat : [L]), ...Flat]
  : T;
// -----------------------

type A = Flat; // [1,2,3]
type B = Flat; // [1,2,3,4,5,6]
type C = Flat; // []
type D = Flat; // [1]
Repeat

复制类型 T 为 C 个元素的元组类型

export type Repeat = C extends F["length"]
  ? F
  : Repeat;

// -----------------------

type A = Repeat; // [number, number, number]
type B = Repeat; // [string, string]
type C = Repeat; // [1]
type D = Repeat; // []
Filter

保留元组类型 T 中的 A 类型

export type Filter = T extends [
  infer L,
  ...infer R
]
  ? Filter
  : F;

// -----------------------

type A = Filter; // [1, 2]
type B = Filter; // ['BFE', 'dev']
type C = Filter; // ['BFE', any, 'dev']
FindIndex

找出 E 类型在元组类型 T 中的下标

export type IsEqual = [T] extends [U]
  ? [U] extends [T]
    ? keyof T extends keyof U
      ? keyof U extends keyof T // 解决结构比较问题
        ? Success
        : Fail
      : Fail
    : Fail
  : Fail;
// IsEqual; any 判断问题

export type FindIndex = T extends [
  infer L,
  ...infer R
]
  ? IsEqual>
  : never;

// -----------------------

type a1 = [any, never, 1, "2", true];
type a2 = FindIndex; // 2
type a3 = FindIndex; // never
TupleToEnum

元组类型转换为枚举类型

import {FindIndex} from "./25.findIndex";

type TupleToEnum = {[K in T[number]]: C extends true ? FindIndex : K;
};

// -----------------------

// 默认情况下,枚举对象中的值就是元素中某个类型的字面量类型
type a1 = TupleToEnum;
// -> {readonly MacOS: "MacOS", readonly Windows: "Windows", readonly Linux: "Linux"}

// 如果传递了第二个参数为 true,则枚举对象中值的类型就是元素类型中某个元素在元组中的 index 索引,也就是数字字面量类型
type a2 = TupleToEnum;
// -> {readonly MacOS: 0, readonly Windows: 1, readonly Linux: 2}
Slice

截取元组中的部分元素

export type Slice = T extends [infer L, ...infer R]
  ? SA["length"] extends S // 如果数组满足开头
    ? EA["length"] extends E
      ? [...F, L] // 如果满足结尾则结束
      : Slice // 满足开头,则放入数组
    : Slice // 不满足开头则累加长度
  : F;

// -----------------------

type A1 = Slice; // [any,never,1]                    从第 0 个位置开始,保留到第 2 个位置的元素类型
type A2 = Slice; // [never,1,'2']                    从第 1 个位置开始,保留到第 3 个位置的元素类型
type A3 = Slice; // [never,1]                        从第 1 个位置开始,保留到第 2 个位置的元素类型
type A4 = Slice; // [1,'2',true,boolean]             从第 2 个位置开始,保留后面所有元素类型
type A5 = Slice; // []                               从第 2 个位置开始,保留后面所有元素类型
type A6 = Slice; // []                               从第 0 个位置开始,保留后面所有元素类型
Splice

删除并且替换部分元素

export type Splice = T extends [infer L, ...infer R]
  ? SA["length"] extends S // 如果数组满足开头
    ? EA["length"] extends E
      ? [...F, ...I, ...T] // 如果满足结尾则, 后面的都要 保留的 + 插入的 + 剩余的
      : Splice // 满足开头,计算删除个数
    : Splice // 不满足开头,保留内容, 并且累加开头长度
  : F;

// -----------------------

type A1 = Splice; // [boolean,null,undefined,never]               从第 0 开始删除,删除 2 个元素
type A2 = Splice; // [string,undefined,never]                     从第 1 开始删除,删除 3 个元素
type A3 = Splice; // [string,1,2,3,null,undefined,never]          从第 1 开始删除,删除 2 个元素,替换为另外三个元素 1,2,3                            从第 0 个位置开始,保留后面所有元素类型

基于结构

OptionalKeys

获取对象类型中的可选属性的联合类型

// 拿出每一个 key 来看下忽略掉后是否能赋予给原来的类型,如果可以,则说明此属性是可选属性
export type OptionalKeys = K extends keyof T
  ? Omit extends T
    ? K
    : never
  : never;

// -------------------------

type a1 = OptionalKeys; // bar
type a2 = OptionalKeys; // bar
type a3 = OptionalKeys; // never
type a4 = OptionalKeys; // foo|flag
type a5 = OptionalKeys; // never
PickOptional

保留一个对象中的可选属性类型

export type PickOptional = Pick>;

// -------------------------
type a1 = PickOptional; // {bar?:string|undefined}
type a2 = PickOptional; // {bar?:string}
type a3 = PickOptional; // {}
type a4 = PickOptional; // {foo?:number,flag?:boolean}
type a5 = PickOptional; // {}
RequiredKeys

获取对象类型中的必须属性的联合类型

export type RequiredKeys = Exclude>;

// ------------------------------

type a1 = RequiredKeys; // foo|flag
type a2 = RequiredKeys; // foo
type a3 = RequiredKeys; // foo|flag
type a4 = RequiredKeys; // never
type a5 = RequiredKeys; // never
PickRequired

保留一个对象中的必须属性

import {RequiredKeys} from "./3.requirredKeys";
export type PickRequired = Pick>;

// ----------------------------

type a1 = PickRequired; // {foo:number|undefined,flag:boolean}
type a2 = PickRequired; // {foo:number}
type a3 = PickRequired; // {foo:number,flag:boolean}
type a4 = PickRequired; // {}
type a5 = PickRequired; // {}
IsNever

判断是否为 never 类型

export type IsNever = [T] extends [never] ? true : false;

// ----------------------

type A = IsNever; // true
type B = IsNever; // false
type C = IsNever; // false
type D = IsNever; // false
IsEmptyType

判断是否为没有属性的对象类型{}

export type IsEmptyType = [keyof T] extends [never]
  ? unknown extends T
    ? false
    : boolean extends T // 排除 object 的情况
    ? true
    : false
  : false;

type x1 = keyof {}; // never
type x2 = keyof object; // never   不能把基础类型赋予给 object
type x4 = keyof unknown; // never  unknown 类型只能赋予给 unknown
type x3 = keyof Object; // toString"|"valueOf

// ----------------------

type A = IsEmptyType; // false
type B = IsEmptyType; // false
type C = IsEmptyType; // true
type D = IsEmptyType; // false
type E = IsEmptyType; // false
type F = IsEmptyType; // false
type G = IsEmptyType; // false
IsAny
type IsAny = 0 extends 1 & T ? true : false;

// 先过滤出 any 和 unknown 来
// any 可以赋予给任何类型,unknown 不可以
export type IsAny = unknown extends T
  ? [T] extends [boolean]
    ? true
    : false
  : false;

// ----------------------

type A = IsAny; // false
type B = IsAny; // true
type C = IsAny; // false
type D = IsAny; // false
Redux Connect
type transformT> = T extends (
  input: Promiseinfer U>
) => PromiseActioninfer S>>
  ? (input: U) => ActionS>
  : T extends (aciton: Actioninfer U>) => Actioninfer S>
  ? (action: U) => ActionS>
  : never;

type ConnectT> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: transform
    T[K]
  >;
};
type F = ConnectModule>;


interface Module {
  count: number;
  message: string;
  asyncMethodT, U>(input: PromiseT>): PromiseActionU>>;
  syncMethodT, U>(action: ActionT>): ActionU>;
}

interface ActionT> {
  payload?: T;
  type: string;
}


type Result = {
  asyncMethodT, U>(input: T): ActionU>;
  syncMethodT, U>(action: T): ActionU>;
};




action 的定义方式,为了测试使用

class Module {
  count = 1;
  message = "hello!";
  asyncMethod(input: Promise) {return input.then((i) => ({
      payload: i,
      type: "asyncMethod",
    }));
  }
  syncMethod(action: Action) {
    return {
      payload: action.payload,
      type: "syncMethod",
    };
  }
}
UnionToIntersection

逆变参数可以传父亲

将联合类型转换为交叉类型



export type UnionToIntersectionT> = (
  T extends any ? (p: T) => any : never
) extends (p: infer R) => any
  ? R
  : never;


type FuncType =
  | ((p: { a: string }) => "人")
  | ((p: { b: boolean }) => "狗")
  | ((p: { c: number }) => "猪");

type T1 = { name: string };
type T2 = { age: number };
type ToIntersectionT> = T extends [(x: infer U) => any, (x: infer U) => any]
  ? U
  : never;
type t3 = ToIntersection[(x: T1) => an y, (x: T2) => any]>;





UnionToTuple

联合类型转换为元组类型

type X = ((p: string) => { a: string }) &
  ((p: number) => { b: string }) &
  ((p: boolean) => { c: number });

function a(a: string): { a: string };
function a(a: number): { b: string };
function a(a: boolean): { c: string };
function a(a: string | number | boolean): { a: string; b: string; c: string } {
  return { a: "123", b: "123", c: "123" };
}

type ParamaterTypeT> = T extends (value: infer R) => any ? R : never;
type R = ParamaterTypeX>;


type FindUnionOneT> = IsAnyT> extends true
  ? any
  : boolean extends T
  ? boolean
  : (T extends any ? (a: (p: T) => any) => any : never) extends (
      a: infer R
    ) => any
  ? R extends (a: infer R1) => any
    ? R1
    : void
  : never;





type UnionToTupleU, Last = FindUnionOneU>> = [U] extends [never]
  ? []
  : [...UnionToTupleExcludeU, Last>>, Last];

type a1 = UnionToTuple1 | 2 | boolean | string>;



type a = UnionToTuple1 | 2 | 3>; 

模板字符串以及装饰器

模板字符串类型

模板字符串类型就是将两个字符串类型值组装在一起返回。使用方式类似于 ES6 中的模板字符串。

基本使用
type name = "hswen";
type sayHello = `hello, ${name}`; 


type Direction = "left" | "right" | "top" | "bottom";
type AllMargin = `marigin-${Direction}`; 


type IColor = "red" | "yellow" | "green";
type ICount = 100 | 200 | 300;
type BookSKU = `${IColor}-${ICount}`; 
通过泛传入类型
type sayHelloT extends string | number | bigint | boolean | null | undefined> =
  `hello, ${T}`; 

type V1 = sayHello"hs">; 
type V2 = sayHello30>; 
type V3 = sayHello123n>; 
type V4 = sayHellotrue>; 
type V5 = sayHellonull>; 
type V6 = sayHelloundefined>; 
type v7 = sayHellostring>; 
type v8 = sayHellonumber>; 


type isChild = V1 extends v7 ? true : false;
映射类型中使用模板字符串
对 key 进行重命名
type Person = { name: string; age: number; address: string };
type RenamePersonT> = {
  [K in keyof T as `re_${K & string}`]: T[K]; 
};
let person: RenamePersonPerson> = {
  re_name: "hs",
  re_age: 30,
  re_address: "回龙观",
};
专用工具类型

Uppercase、Lowercase、Capitalize、Uncapitalize

type Person = { name: string; age: number; address: string };
type PersonWithGetterT> = {
  [K in keyof T as `get${Capitalizestring & K>}`]?: () => T[K];
};
let person: Person = { name: "hs", age: 39, address: "回龙观" };
let personGetter: PersonWithGetterPerson> = {
  getName() {
    return person.name;
  },
};
模式匹配
type GetFristNameS extends string> = S extends `${infer F} ${infer O}` ? F : S;
type x = GetFristName"hs wen">; 

装饰器

装饰器本质就是一个函数,只能在类以及类成员上使用。TypeScript 中的装饰器可以分为类装饰器、方法装饰器、访问符装饰器、属性装饰器以及参数装饰器

类装饰器

类装饰器是直接作用在类上的装饰器,它在执行时的入参只有一个,即是这个类本身。如果装饰器函数中返回一个新的类,那么即是这个类的子类,这个子类可以用于重写父类。

const Decorator = T extends { new (...args: any[]): {} }>(target: T) => {
  (target as any).type = "动物";
  (target as any).getType = function () {
    return this.type;
  };
  Object.assign(target.prototype, {
    eat() {
      console.log("eat");
    },
    drink() {
      console.log("drink");
    },
  });
};
interface Animal {
  eat(): void;
  drink(): void;
}
@Decorator
class Animal {}
const animal = new Animal();


animal.eat();
animal.drink();

console.log((Animal as any).getType());

通过返回子类的方式进行扩展

const OverrideAnimal = (target: any) => {
  return class extends target {
    
    eat() {
      super.eat();
      console.log("Override eat");
    }
    drink() {
      console.log("Overrided drink");
    }
  };
};

@OverrideAnimal
class Animal {
  eat() {
    console.log("eat");
  }
}
const animal = new Animal();
animal.eat();
(animal as any).drink();
方法装饰器

方法装饰器的入参包括类的原型、方法名以及方法的属性描述符(PropertyDescriptor)。

function Enum(isEnum: boolean) {
  
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    
    
    
    
    descriptor.enumerable = isEnum; 
    let originalEat = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log("prev-eat");
      originalEat.call(this, ...args);
      console.log("next-eat");
    };
  };
}

class Animal {
  @Enum(true)
  eat() {
    console.log("eat");
  }
}
const animal = new Animal();
animal.eat();
访问符装饰器

访问符装饰器本质上仍然是方法装饰器,它们使用的类型定义相同。访问符装饰器只能应用在 getter / setter 的其中一个(装饰器入参中的属性描述符都会包括 getter 与 setter 方法:)。

function ValueToUpper(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.set;
  descriptor.set = function (newValue: string) {
    original?.call(this, newValue.toUpperCase());
  };
}

class Animal {
  private _value!: string;
  @ValueToUpper 
  get value() {
    return this._value;
  }
  set value(newValue: string) {
    this._value = newValue;
  }
}
const animal = new Animal();
animal.value = "ok";
console.log(animal.value);
属性装饰器

属性装饰器在独立使用时能力非常有限,可以在类的原型上赋值来修改属性。

function ToUpper(target: any, key: string) {
  let val = "";
  
  Object.defineProperty(target, key, {
    
    enumerable: true,
    get() {
      return val.toUpperCase();
    },
    set(newValue) {
      val = newValue;
    },
  });
}

class Animal {
  @ToUpper
  public name: string = "Animal"; 
}
const animal = new Animal();
console.log(animal);
参数装饰器

参数装饰器包括了构造函数的参数装饰器与方法的参数装饰器,它的入参包括类的原型、参数所在的方法名与参数在函数参数中的索引值,独立使用能力依旧有限。

function Params(target: any, key: string, index: number) {
  
  console.log(target, key, index);
}
class Animal {
  public name: string = "Animal"; 
  play(@Params val: string) {
    console.log(val);
  }
}
装饰器执行流程
function Echo(val: string): any {
  return () => {
    console.log(val);
  };
}
@Echo("类装饰器 1") 
@Echo("类装饰器 2") 
@Echo("类装饰器 3") 
@Echo("类装饰器 4") 
class Flow {
  constructor(@Echo("构造函数参数装饰器") str: string) {}
  @Echo("静态方法装饰器")
  static getType(@Echo("静态方法参数装饰器") str: string) {
    return this.type;
  }
  @Echo("静态属性装饰器")
  static type = "hello";

  @Echo("实例方法装饰器")
  handler(@Echo("实例方法参数装饰器") str: string) {}

  @Echo("实例属性装饰器")
  name!: string;

  @Echo("属性访问装饰器")
  get value() {
    return "hello";
  }
}















  • 方法装饰器,我们通常进行方法执行前后的逻辑注入。
  • 属性、参数装饰器,我们通常只进行信息注册,委托别人处理。

反射元数据 Reflect Metadata

元数据:用于描述数据的数据,将信息存到 map 表中,最终统一操作。

反射的核心是:在程序运行时去检查以及修改程序行为,允许程序在运行时获取自身的信息。

元数据命令式定义
import "reflect-metadata";
class Animal {
  static type = "哺乳类";
  eat() {}
}
Reflect.defineMetadata("Class", "Animal metadata", Animal);
Reflect.defineMetadata("Class property", "type metadata", Animal, "type");
Reflect.defineMetadata("proto method", "eat metadata", Animal.prototype, "eat");
/*
 => 
WeakMap => {
            Animal:{undefined:{'Class' => 'Animal metadata'},
                type:{'Class property' => 'type metadata'}
            },
            Animal.prototype:{eat:{'proto method' => 'eat metadata'},
            }
          }
*/
// 取 data
console.log(Reflect.getMetadata("Class", Animal));
console.log(Reflect.getMetadata("Class property", Animal, "type"));
console.log(Reflect.getMetadata("proto method", Animal.prototype, "eat"));

// 取 key
console.log(Reflect.getMetadataKeys(Animal));
console.log(Reflect.getMetadataKeys(Animal, "type"));
console.log(Reflect.getMetadataKeys(Animal.prototype, "eat"));
元数据声明式定义
@Reflect.metadata("Class", "Animal metadata")
class Animal {@Reflect.metadata("Class property", "type metadata")
  static type = "哺乳类";
  @Reflect.metadata("proto method", "eat metadata")
  eat() {}
}

// 在类装饰器中进行数据的消费
console.log(Reflect.getMetadata("Class", Animal));
console.log(Reflect.getMetadata("Class property", Animal, "type"));
console.log(Reflect.getMetadata("proto method", Animal.prototype, "eat"));
生成额外的 metadata

开启 "emitDecoratorMetadata": true 后自动生成基于类型的元数据。

// 通过原型
console.log(Reflect.getMetadata("design:type", Animal.prototype, "eat"));
console.log(Reflect.getMetadata("design:paramtypes", Animal.prototype, "eat"));
console.log(Reflect.getMetadata("design:returntype", Animal.prototype, "eat"));

// 通过实例
console.log(Reflect.getMetadata("design:type", new Animal(), "eat"));
console.log(Reflect.getMetadata("design:paramtypes", new Animal(), "eat"));
console.log(Reflect.getMetadata("design:returntype", new Animal(), "eat"));
Required 必填属性实战
import "reflect-metadata";

const REQUIRED_KEY = Symbol("required_key");
function Required(): PropertyDecorator {return (target, prop) => {const requiredkeys: string[] =
      Reflect.getMetadata(REQUIRED_KEY, target) || [];
    // 设置元数据
    Reflect.defineMetadata(REQUIRED_KEY, [...requiredkeys, prop], target);
  };
}
class Person {@Required()
  name!: string;
  @Required()
  age!: number;
}
function validate(instance: any) {let exisitsKeys = Reflect.ownKeys(instance); // 获取已经存在的属性
  let requiredKeys = Reflect.getMetadata(REQUIRED_KEY, instance) || [];

  for (const key of requiredKeys) {if (!exisitsKeys.includes(key)) {throw new Error(key + "is required");
    }
  }
}

// 1)先记录哪些属性为必填属性
// 2) 在查询实例上哪个属性没有
const person = new Person();
person.name = "hh";
person.age = 30;

validate(person); // 校验属性
TypeValidation 类型校验
const VALIDATION_KEY = Symbol("VALIDATION_KEY");
enum Type {
  String = "string",
  Number = "number",
}
function ValueType(type: Type) {return (target: any, prop: string) => {
    // 给某个属性添加元数据
    Reflect.defineMetadata(VALIDATION_KEY, type, target, prop);
  };
}
class Person {@ValueType(Type.Number) // 值的类型应为 number
  @Required()
  age!: number;
}
const instance = new Person();
// @ts-ignore
instance.age = "18";
function validate(instance: any) {let exisitsKeys = Reflect.ownKeys(instance); // 获取已经存在的属性
  let requiredKeys = Reflect.getMetadata(REQUIRED_KEY, instance) || [];
  for (let key of exisitsKeys) {let validations = Reflect.getMetadata(VALIDATION_KEY, instance, key);
    if (validations) {
      // 看存在的类型是否满足
      if (typeof instance[key] !== validations) {throw new Error(`${String(key)} expect ${validations}`);
      }
    }
  }
  // 校验必填属性,看实例上是否存在需要的必填属性
  for (const key of requiredKeys) {if (!exisitsKeys.includes(key)) {throw new Error(key + "is required");
    }
  }
}
validate(instance);

控制反转

  • 控制正转:我们去超市购物,结账时我们需要一个个自己扫描商品条形码,填写数量进行付款。整个过程由我自己控制
  • 控制反转:我门去超市购物,把车推到收款区,收银员去识别条形码,最后我来付款。控制权就被反转了。(失去了控制权)

IoC(Inversion of Control)即控制反转,在开发中是一种设计思想。传统编程中,我们自己在对象内部创建依赖对象,即正向控制。而在 IoC 中,我们将对象的创建交给容器来控制,对象被动接受依赖,从而反转了控制关系。(解决问题:类之间的耦合度高,难以测试和重用,依赖关系的问题)

interface Monitor {}
interface Host {}
class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {
  public monitor: Monitor;
  public host: Host;
  constructor() {this.monitor = new Monitor27inch();
    this.host = new AppleHost();}
  bootstrap() {console.log("启动电脑");
  }
}
const computer = new Computer();
computer.bootstrap();
// 组装电脑时想使用不同的零件如何实现?

根据需要手动创建并且传入零件(手工维护依赖关系)

interface Monitor {}
interface Host {}
class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {constructor(public monitor: Monitor, public host: Host) {}
  bootstrap() {console.log("启动电脑");
  }
}
const monitor27 = new Monitor27inch();
const appleHost = new AppleHost();
const computer = new Computer(monitor27, appleHost);
computer.bootstrap();

模拟容器

interface Monitor {}
interface Host {}

class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {constructor(public monitor: Monitor, public host: Host) {}
  bootstrap() {console.log("启动电脑");
  }
}
class Container {private instances = new Map();
  bind(key: string, creator: () => T) {if (!this.instances.has(key)) {this.instances.set(key, creator());
    }
    return this.instances.get(key) as T;
  }
  resolve(key: string): T {return this.instances.get(key) as T;
  }
}
const container = new Container();
container.bind("Monitor", () => new Monitor27inch());
container.bind("Host", () => new AppleHost());
const computer = container.bind(
  "Computer",
  () => new Computer(container.resolve("Monitor"), container.resolve("Host"))
);
computer.bootstrap();

依赖注入

DI 是 IoC 的具体体现,它是一种模式,它通过容器动态地将某个组件所需的依赖注入到组件中,而无需硬编码在组件内部。

如果代码是这个样子的,那就非常完美了~

@Provide("Monitor")
class Monitor27inch {}
@Provide("Host")
class AppleHost {}

@Provide("Computer")
class Computer {@Inject("Monitor")
  monitor!: Monitor27inch;

  @Inject("Host")
  host!: AppleHost;

  bootstrap() {console.log("启动电脑");
  }
}

这种模式让我们可以专注于组件自身的逻辑,而不需要关心具体的依赖资源如何创建和提供。容器负责在运行时解决依赖关系,从而使代码更具可维护性和灵活性。

class Container {private instances = new Map(); // 存储类 和 类的创造器
  public properties = new Map(); // 存储属性
  bind(key: string, creator: () => T) {if (!this.instances.has(key)) {this.instances.set(key, creator());
    }
    return this.instances.get(key) as T;
  }
  resolve(key: string): T {let instance = this.instances.get(key);
    for (let property of this.properties) {
      // 循环所有的属性
      let [key, ServiceKey] = property;
      let [classKey, propKey] = key.split("-"); // 类的名字和属性名
      if (instance.constructor.name !== classKey) {
        // 如果不是当前类的
        continue;
      }
      const target = this.resolve(ServiceKey); // 解析依赖
      instance[propKey] = target;
    }
    return instance as T;
  }
}
const container = new Container();

@Provide("Monitor")
class Monitor27inch {}
@Provide("Host")
class AppleHost {}

@Provide("Computer")
class Computer {@Inject("Monitor")
  monitor!: Monitor27inch;

  @Inject("Host")
  host!: AppleHost;

  bootstrap() {console.log("启动电脑");
  }
}
// 注册到容器中
function Provide(key: string) {return function (Target: any) {
    // 保存类的名字和类的创建器
    container.bind(key ?? Target.name, () => new Target());
  };
}
// 注入到当前类中
function Inject(InjectKey: string) {return function (target: any, key: string) {
    // 保存注入的属性信息
    container.properties.set(`${target.constructor.name}-${key}`, InjectKey);
  };
}
const computer = container.resolve("Computer");
computer.bootstrap();

依赖注入实战

import "reflect-metadata";
function methodDecorator(method: string) {return function (path: string) {return function (target: any, key: string, descriptor: PropertyDescriptor) {Reflect.defineMetadata("method", method, descriptor.value);
      Reflect.defineMetadata("path", path, descriptor.value);
    };
  };
}
export const Controller = (path?: string) => {return function (target: any) {Reflect.defineMetadata("path", path ?? "", target);
  };
};
export const Get = methodDecorator("get");
export const Post = methodDecorator("post");
@Controller("/article")
class ArticleController {@Get("/detail")
  getDetail() {return "get detail";}
  @Post("/add")
  addArticle() {return "post add";}
}
function createRoutes(instance: any) {const prototype = Reflect.getPrototypeOf(instance)!;
  const rootPath = Reflect.getMetadata("path", prototype.constructor);
  const methods = Reflect.ownKeys(prototype).filter((item) => item !== "constructor"
  );
  const routes = methods.map((method) => {const requestHandler = (prototype as any)[method];
    const requestPath = Reflect.getMetadata("path", requestHandler); // 获得路径
    const requestMethod = Reflect.getMetadata("method", requestHandler);
    return {requestPath: `${rootPath}${requestPath}`,
      requestHandler,
      requestMethod,
    };
  });
  return routes;
}
const routes = createRoutes(new ArticleController());
console.log(routes);

axios 核心实现

axios

import axios, { AxiosRequestConfig, AxiosResponse } from "axios";

const baseURL = "http://localhost:8080";


interface Person {
  name: string;
  age: number;
}
let person: Person = { name: "hswen", age: 30 };


let requestConfig: AxiosRequestConfig = {
  method: "get",
  url: baseURL + "/get",
  params: person,
};


axios(requestConfig)
  .then((response: AxiosResponse) => {
    return response.data;
  })
  .catch((error: any) => {
    console.log(error);
  });

创建 axios 基本结构

axios/index.ts

class Axios {
  request() {}
}
function createInstance() {
  
  const context = new Axios();
  
  const instance = Axios.prototype.request.bind(context);
  return instance;
}


const axios = createInstance();
export default axios;

为了编写代码方便,我们将 Axios 类单独拿出去定义

axios/Axios.ts

class Axios {
  request() {}
}
export default Axios;

创建请求及响应类型

axios/types.ts

AxiosRequestConfig
export type Methods =
  | "get"
  | "GET"
  | "post"
  | "POST"
  | "put"
  | "PUT"
  | "delete"
  | "DELETE"
  | "options"
  | "OPTIONS";

export interface AxiosRequestConfig {
  url?: string;
  method?: Methods;
  params?: any;
}
AxiosResponse
export interface AxiosResponseT = any> {
  data: T;
  status: number;
  statusText: string;
  headers: Recordstring, any>;
  config: AxiosRequestConfig;
  request?: XMLHttpRequest;
}

在入口文件中导出所有类型,export * from "./types";

编写请求方法

编写 request
export interface AxiosInstance {
  T = any>(config: AxiosRequestConfig): PromiseAxiosResponseT>>;
}

用于描述 request 方法

const instance: AxiosInstance = Axios.prototype.request.bind(context);
编写请求逻辑
import {AxiosRequestConfig, AxiosResponse} from "./types";
import qs from "qs";
import parseHeader from "parse-headers";
class Axios {request(config: AxiosRequestConfig): Promise> {
    // 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。// todo...
    return this.dipsatchRequest(config);
  }
  dipsatchRequest(config: AxiosRequestConfig): Promise> {return new Promise(function (resolve, reject) {let { method, url, params} = config;
      const request = new XMLHttpRequest();

      // get 请求参数
      if (params) {if (typeof params === "object") {params = qs.stringify(params);
        }
        url += (url!.indexOf("?") > -1 ? "&" : "?") + params;
      }
      request.open(method!, url!, true);

      request.responseType = "json";
      request.onreadystatechange = function () {if (request.readyState === 4 && request.status !== 0) {if (request.status>= 200 && request.status  = {
              data: request.response ? request.response : request.responseText,
              status: request.status,
              statusText: request.statusText,
              headers: parseHeader(request.getAllResponseHeaders()),
              config,
              request,
            };
            resolve(response);
          } else {reject("请求失败~~~");
          }
        }
      };
      request.send();});
  }
}
export default Axios;

处理 Post 请求

请求参数
let requestConfig: AxiosRequestConfig = {
  method: "post",
  url: baseURL + "/post",
  data: person,
  headers: {"content-type": "application/json",},
};
修改配置接口
export interface AxiosRequestConfig {
  url?: string;
  method?: Methods;
  params?: any;
  headers?: Record;
  data?: Record;
}
修改发送逻辑
if (headers) {for (let key in headers) {request.setRequestHeader(key, headers[key]);
  }
}
let body: string | null = null;
if (data) {body = JSON.stringify(data);
}
request.send(body);

错误处理

网络异常错误
request.onerror = function () {reject("net::ERR_INTERNET_DISCONNECTED");
};

可以通过 onerror 监控网络产生的异常。

超时处理
export interface AxiosRequestConfig {
  // ...
  timeout?: number; // 增加超时时间
}

请求参数

let requestConfig: AxiosRequestConfig = {
  method: "post",
  url: baseURL + "/post_timeout?timeout=3000", // 3s 后返回结果
  data: person,
  headers: {"content-type": "application/json",},
  timeout: 1000, // 1s 后就超时
};

设置超时时间

if (timeout) {
  request.timeout = timeout;
  request.ontimeout = function () {reject(`Error: timeout of ${timeout}ms exceeded`);
  };
}
状态码错误

请求参数

let requestConfig: AxiosRequestConfig = {
  method: "post",
  url: baseURL + "/post_status?code=401", // 3s 后返回结果
  data: person,
  headers: {"content-type": "application/json",},
};

设置错误信息

request.onreadystatechange = function () {if (request.readyState === 4 && request.status !== 0) {if (request.status>= 200 && request.status 

拦截器

let requestConfig: AxiosRequestConfig = {
  method: "post",
  url: baseURL + "/post",
  data: person,
  headers: {
    "content-type": "application/json",
    name: "", // 用于记录拦截器的执行顺序
  },
};
拦截器执行顺序
// 请求拦截器是倒序执行的,先放入的拦截器最后执行
let request = axios.interceptors.request.use((config) => {
    config.headers.name += "a";
    return config;
  },
  (err) => Promise.reject(err)
);
axios.interceptors.request.use((config) => {
  config.headers.name += "b";
  return config;
});
axios.interceptors.request.use((config) => {
  config.headers.name += "c";
  return config;
});
axios.interceptors.request.eject(request); // 放入的可以抛出来
// 响应拦截器是正序执行的,先放入的拦截器先执行
let response = axios.interceptors.response.use((response) => {
  response.data.name += "a";
  return response;
});
axios.interceptors.response.use((response) => {
  response.data.name += "b";
  return response;
});
axios.interceptors.response.use((response) => {
  response.data.name += "c";
  return response;
});
axios.interceptors.response.eject(response);
拦截器 promise 写法
axios.interceptors.request.use((config) => {return new Promise((resolve) => {setTimeout(() => {
      config.headers!.name += "c";
      resolve(config);
    }, 3000);
  });
  return Promise.reject("失败了");
});
拦截器类型定义
// 强制将 headers 属性进行重写,变为非可选
export interface InternalAxiosRequestConfig extends AxiosRequestConfig {headers: Record;
}
export interface AxiosInstance {(config: AxiosRequestConfig): Promise>;
  interceptors: {request: AxiosInterceptorManager;
    response: AxiosInterceptorManager;
  };
}

AxiosInterceptorManager 实现

type OnFulfilled = (value: V) => V | Promise;
type OnRejected = (error: any) => any;

export interface Interceptor {onFulfilled?: OnFulfilled;
  onRejected?: OnRejected;
}
class AxiosInterceptorManager {public interceptors: Array | null> = [];
  use(onFulfilled?: OnFulfilled, onRejected?: OnRejected): number {
    this.interceptors.push({
      onFulfilled,
      onRejected,
    });
    return this.interceptors.length - 1;
  }
  eject(id: number) {if (this.interceptors[id]) {this.interceptors[id] = null;
    }
  }
}
export default AxiosInterceptorManager;
拦截器执行原理

缺少属性“interceptors”,但类型“AxiosInstance”中需要该属性

const instance: AxiosInstance = Axios.prototype.request.bind(context);
class Axios {
  public interceptors = {request: new AxiosInterceptorManager(),
    response: new AxiosInterceptorManager(),};
}
function createInstance() {const context = new Axios();
  let instance = Axios.prototype.request.bind(context);
  // 3. 将实例属性合并到 request 中
  instance = Object.assign(instance, context);
  return instance as AxiosInstance;
}

构建执行链

// 存放执行链路
const chain: (| Interceptor
  | Interceptor
)[] = [{ onFulfilled: this.dipsatchRequest}];

this.interceptors.request.interceptors.forEach((interceptor) => {interceptor && chain.unshift(interceptor);
});

this.interceptors.response.interceptors.forEach((interceptor) => {interceptor && chain.push(interceptor);
});

let promise: Promise =
  Promise.resolve(config);

while (chain.length) {const { onFulfilled, onRejected} = chain.shift()!; // 从头部删除元素
  promise = promise.then(onFulfilled as (v: AxiosRequestConfig | AxiosResponse) => any,
    onRejected
  );
}
// todo...
return promise as Promise>;

合并配置

创建默认值对象
// 默认配置
const defaults: InternalAxiosRequestConfig = {
  method: "get",
  timeout: 0,
  headers: {
    common: {accept: "application/json",},
  },
};

// 允许用户给 defaults 对象添加不同方法的默认值
let allMethods = ["delete", "get", "head", "patch", "post", "put"];
allMethods.forEach((method: string) => {defaults.headers[method] = {};});
设置请求 headers
if (headers) {for (let key in headers) {
    // 如果是 common 或是方法 就将对象合并
    if (key === "common" || key === config.method) {for (let key2 in headers[key]) {request.setRequestHeader(key2, headers[key][key2]);
      }
    } else {if (!allMethods.includes(key)) {request.setRequestHeader(key, headers[key]);
      }
    }
  }
}
请求与响应转换

类型声明

export interface AxiosRequestConfig {
  url?: string;
  method?: Methods;
  params?: any;
  headers?: Record;
  data?: Record;
  timeout?: number; // 增加超时时间

  // 转化请求及响应类型定义
  transformRequest?: (data: Record,
    headers: Record
  ) => any;
  transformResponse?: (data: any) => any;
}

方法实现

// 默认配置
const defaults: InternalAxiosRequestConfig = {
  method: "get",
  headers: {
    common: {accept: "application/json",},
  },
  // 请求前执行此方法
  transformRequest: (data: Record,
    headers: Record
  ) => {headers["content-type"] = "application/x-www-form-urlencoded";
    return qs.stringify(data);
  },
  // 获取后执行此方法
  transformResponse(data: any) {if (typeof data == "string") data = JSON.parse(data);
    return data;
  },
};
request(config: AxiosRequestConfig): Promise> {
  // 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。config.headers = Object.assign(this.defaults.headers, config.headers);

  // 合并(请求、响应)转化方法
  config.transformRequest =
    config.transformRequest || this.defaults.transformRequest;
  config.transformResponse =
    config.transformResponse || this.defaults.transformResponse;

  if (config.transformRequest && config.data) {config.data = config.transformRequest(config.data, (config.headers = {}));
  }
}
request.onreadystatechange = () => {if (request.readyState === 4 && request.status !== 0) {if (request.status>= 200 && request.status 

请求终止

cancelToken 的使用
const CancelToken = axios.CancelToken;
const source = CancelToken.source(); // 创建取消 token

let requestConfig: AxiosRequestConfig = {
  method: "post",
  url: baseURL + "/post",
  data: person,
  cancelToken: source.token, // 请求时携带 token
};
axios(requestConfig)
  .then((response: AxiosResponse) => {console.log(response.data);
    return response.data;
  })
  .catch((error: any) => {if (axios.isCancel(error)) {return console.log("取消:" + error);
    }
    console.log(error);
  });

source.cancel("用户取消请求");
取消实现原理
export class Cancel {constructor(public message: string) {}}
export function isCancel(value: any): value is Cancel {return value instanceof Cancel;}

// 取消的实现
export class CancelTokenStatic {public resolve!: (val: Cancel) => void;
  source() {
    return {
      // token 就是一个 promise
      token: new Promise((resolve) => {this.resolve = resolve;}),
      // 让这个 promise 成功,并且传入中断的原因
      cancel: (reason: string) => {this.resolve(new Cancel(reason));
      },
    };
  }
}
声明所需类型

axios/types.ts

export interface AxiosInstance {(config: AxiosRequestConfig): Promise>;
  interceptors: {request: AxiosInterceptorManager;
    response: AxiosInterceptorManager;
  };
  CancelToken: CancelTokenStatic; // 取消 token
  isCancel: typeof isCancel; // 请求是否是被取消
}
export type CancelToken = ReturnType["token"];
export interface AxiosRequestConfig {
  // ...
  cancelToken?: CancelToken;
}

axios/Axios.ts

if (config.cancelToken) {config.cancelToken.then((reason: Cancel) => {request.abort();
    reject(reason);
  });
}
let body: string | null = null;
if (data) {body = JSON.stringify(data);
}
request.send(body);
//.....

装包和拆包

装包

将每个属性都被包装成了一个代理对象,用于访问和设置原始对象的属性值。

let props = {
  name: "jiangwen",
  age: 30,
};
type Proxy = {get(): T;
  set(value: T): void;
};
type Proxify = {[P in keyof T]: Proxy;
};
function proxify(obj: T): Proxify {let result = {} as Proxify;
  for (let key in obj) {let value = obj[key];
    result[key] = {get() {return value;},
      set: (newValue) => (value = newValue),
    };
  }
  return result;
}
let proxpProps = proxify(props);

拆包

function unProxify(proxpProps: Proxify): T {let result = {} as T;
  for (let key in proxpProps) {let value = proxpProps[key];
    result[key] = value.get();}
  return result;
}
let proxy = unProxify(proxpProps);

axios 请求方法封装

import axios, {AxiosRequestConfig, AxiosInstance, AxiosResponse} from "axios";
// 用 axios 进行二次封装在使用  目的就是添加一些默认的配置和拦截器

// 一般后端返回的类型都是固定的
export interface ResponseData {
  code: number;
  data?: T;
  msg?: string;
}
class HttpRequest {
  public baseURL = "http://localhost:3000/api";
  public timeout = 3000;
  public request(options: AxiosRequestConfig) {
    // 能自动推导就不要自己写
    const instance = axios.create();
    options = this.mergeOptions(options); // 合并后的选项
    this.setInterceptors(instance);

    return instance(options); // 可以发请求了
  }
  public setInterceptors(instance: AxiosInstance) {
    instance.interceptors.request.use((config) => {config.headers!["token"] = "xxx";
        return config;
      },
      (err) => {return Promise.reject(err);
      }
    );
    instance.interceptors.response.use((res: AxiosResponse) => {
        // res.data.data
        let {code} = res.data;
        if (code !== 0) {return Promise.reject(res);
        }
        return res;
      },
      (err) => {return Promise.reject(err);
      }
    );
  }
  mergeOptions(options: AxiosRequestConfig) {
    return Object.assign({ baseURL: this.baseURL, timeout: this.timeout},
      options
    );
  }
  public get(url: string, data: any): Promise> {
    return this.request({
      method: "get",
      url,
      params: data,
    })
      .then((res) => {return Promise.resolve(res.data);
      })
      .catch((err) => {return Promise.reject(err);
      });
  }

  public post(url: string, data: any): Promise> {
    return this.request({
      method: "post",
      url,
      data,
    })
      .then((res) => {return Promise.resolve(res.data);
      })
      .catch((err) => {return Promise.reject(err);
      });
  }
}

const http = new HttpRequest();
http
  .post("/login", {
    username: "123",
    password: "123",
  })
  .then((res) => {res.data?.username;})
  .catch((err) => {err;});

TSConfig 详解

1.Language and Environment 语言和环境

语言和环境
target 指定最终生成的代码语言版本,更改 target 时会引入对应的 lib。例如指定为 es5 时,我们使用 includes 语法会发生异常,提示找不到对应的 lib。当更改为 es6 时,会自动引入对应的lib.2015.core.d.ts
lib 手动配置需要引入的类库, 例如配置 DOM,可以在页面中使用浏览器属性。同时还需手动指定 target 所配置的类库。
jsx 常见的属性有 react(编译后生成React.createElement 方法)、react-jsx(编译后生成自动导入语法)、preserve(不进行转化,常用于 vue 中的 tsx)
experimentalDecorators 启用装饰器实验性语法
emitDecoratorMetadata 启用 metadata 生成元数据相关逻辑
jsxFactory 生成 react 对应的 React.createElement 或者 preact 中的 h 方法。需要在 "jsx": "react" 时使用。
jsxFragmentFactory 生成 react 对应的 React.Fragment 或者 preact 中的 Fragment。需要在 "jsx": "react" 时使用。文档碎片
jsxImportSource 配置 jsx 对应导入模块的路径,需要在 "jsx": "react-jsx" 时使用。
reactNamespace 生成 createElement 调用的命名空间,默认是React
noLib 禁用默认导入的所有 lib
useDefineForClassFields 使用 defineProperty 来定义类中的属性
moduleDetection 模块发现,设置为 force 时所有内容均被当做模块。其它两种模式只会将带有 importexport 的识别为模块。

2.Modules 模块相关

1.module

控制最终 JavaScript 产物使用的模块标准 CommonJsES6ESNext以及 NodeNext AMDUMDSystem

2.rootDir

项目文件的根目录,默认推断为包含所有 ts 文件的文件夹。配合 outDir 可以看最终的输出结果。

  • 如果指定后只会根据指定的路径进行编译输出。

3.moduleResolution

配置模块解析方式 nodeClassicbundler

  • Classic 下的模块 import a from "a"; 导入时会查找 ./a.ts(递归往上找同名文件)。不推荐使用
  • node:不支持exports
  • node16 / nodenext强制使用相对路径模块时必须写扩展名
  • bundler:既能使用 exports 声明类型的同时,也可以使用相对路径模块不写扩展名。

4.baseUrl

定义文件进行解析的根目录,它通常会是一个相对路径,然后配合 tsconfig.json 所在的路径来确定根目录的位置。

// baseUrl:'./'
import a from "src/a";
// 默认以 tsconfig 所在的路径进行解析

5.paths

类似于 alias,支持通过别名的方式进行导入。

TypeScript 进阶

"paths": {"@/shared/*": ["./src/shared/*"]
}
import a from "@/shared/isString";

6.rootDirs

实现虚拟目录,告诉 TS 将这些模块视为同一层级下,但不会影响最终输出结果。可用于映射声明文件。"rootDirs":["src/style","src/typings"]

TypeScript 进阶

var.module.scss

:export {
  color: red;
  border: 2px;
}

var.module.scss.d.ts

interface IScss {
  color: string;
  border: string;
}
const IScss: IScss;
export default IScss;

7.typeRoots

默认情况下,TypeScript 会在 node_modules/@types 下查找类型定义文件,可以通过设置 typeRoots 选项指定类型查找的目录。

{"typeRoots": ["./node_modules/@types", "./typings"]
  "types": ["jquery" // 仅添加哪些声明文件]
},
"include": ["src/**/*"] // 指定查找目录

8.allowUmdGlobalAccess

允许 umd 模块全局访问 export as namespace _;,关闭后需要导入模块后才能访问。

@types/lodash/index.d.ts

declare const _ = _;
declare namespace _ {export type flatten = () => void;
}
export as namespace _; // 将这个命名空间变成全局的不需要导入即可使用
export = _; // 为了用户可以导入
console.log(_); // 可以直接访问

如果文件不在 @types 目录下,需要配置 include 包含此文件。

9.moduleSuffixes

模块增添后缀进行查找[".controller", ".service"]

TypeScript 进阶

10.allowImportingTsExtensions

默认不允许,开启后在相对导入时就允许使用扩展名.ts.mts.tsx,注意要同时启用 --noEmit 或者 --emitDeclarationOnly,因为这些文件导入路径还需要被构建工具进行处理后才能正常使用。

import a from "./a.mts";

11.resolvePackageJsonExports

强制 TypeScript 在从 node_modules 中的包中读取时查询 package.json 文件的 exports 字段。在 moduleResolution 这个值为 node16, nodenext, 和 bundler 时默认开启。

{
  "name":"my-package",
  "exports":{
      ".":{
          "types":"./index.d.ts", // 声明文件
          "import":"./index.mjs", // import 导入的方式
          "require": "./index.js" // requie 导入的方式
      }
  }
}

12.resolvePackageJsonImports

强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的 imports 字段。

"imports": {"#dep/*.js": "./src/utils/*.js"}

13.customConditions

获取当 TypeScript 从 package.json 的导出或导入字段解析时要考虑的附加条件列表。

14.resolveJsonModule

启用了这一配置后,你就可以直接导入 Json 文件,并对导入内容获得完整的基于实际 Json 内容的类型推导。

15.allowArbitraryExtensions

是否以{file basename}.d.{extension} 的形式查找该路径的声明文件。

  • 文件是 app.rc 则声明文件是app.d.rc.ts
declare const style: {
  color: string;
  background: string;
};
export default style;

16.noResolve

不解析文件导入和三斜线指令。

模块相关
module 指定编译后采用的模块方式
rootDir 项目文件的根目录,默认推断为包含所有 ts 文件的文件夹。配合 outDir 可以看最终的输出结果。
moduleResolution 按照 node 方式进行模块解析。
baseUrl 配置项目解析的根目录,配置后可以直接通过根路径的方式导入模块。
paths 路径别名配置 "@/utils/*": ["src/utils/*"]。可以使用相对路径,也可以配置baseUrl 指定相对路径
rootDirs 实现虚拟目录,告诉 TS 将这些模块视为同一层级下,但不会影响最终输出结果。可用于映射声明文件。"rootDirs":["src/a","src/b"]
typeRoots 指定类型查找的目录node_modules/@types./typings
types 手动指定 node_modules/@types 下需要加载的类型。
allowUmdGlobalAccess 允许 umd 模块全局访问 export as namespace _;
moduleSuffixes 模块增添后缀进行查找[".module", ".service"]
allowImportingTsExtensions 在相对导入时就允许使用 ts 的扩展名,注意要同时启用 --noEmit 或者 --emitDeclarationOnly,因为这些文件导入路径还需要被构建工具进行处理后才能正常使用。
resolvePackageJsonExports 强制 TypeScript 在从 node_modules 中的包中读取时查询 package.json 文件的 exports 字段
resolvePackageJsonImports 强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的 imports 字段。
customConditions 自定义条件,基本用不到
resolveJsonModule 解析 json 模块
allowArbitraryExtensions 是否以{file basename}.d.{extension} 的形式查找该路径的声明文件。
noResolve 不解析文件导入和三斜线指令

3.JS 支持

javascript 相关
allowJs 在开启此配置后,可在 .ts 文件中去导入 .js / .jsx 文件。
checkJs 检查 js 文件,也可以通过 @ts-check
maxNodeModuleJsDepth “node_modules”检查 JavaScript 文件的最大文件夹深度。就是 node_modules 向上查找的层级

TypeScript 进阶

4.Emit 输出相关

1.declaration

declaration 接受一个布尔值,即是否产生声明文件。默认不生产

2.declarationMap

引入第三方模块时,默认会查找 .d.ts 文件,配置 declarationMap 后,可以映射到原始的 ts 文件。发布 npm 包时并不会携带这些文件

3.emitDeclarationOnly

此配置会让最终构建结果只包含构建出的声明文件(.d.ts),而不会包含 .js 文件

4.sourceMap

创建 ts 对应的 .map 文件

5.inlineSourceMap

内嵌 sourcemap,不能与 sourceMap 属性连用

6.outFile

将所有结果打包到一个文件中(指定文件名),仅支持 amdsystem模块

7.outDir

将所有生成的文件发射到此目录中

8.removeComments

移除 ts 文件内的注释

9.noEmit

在编译过程中不生成文件,但是编译过程中会进行类型检测。

10.importHelpers

基于 target 进行语法降级,往往需要一些辅助函数,将新语法转换为旧语法的实现。启用 importHelpers 配置,这些辅助函数就将从 tslib 中导出而不是在源码中定义。

TypeScript 进阶

需要安装 tslib,并且开启moduleResolution 选项。

11.importsNotUsedAsValues

是否保留导入后未使用的导入值,默认则删除。此属性被 verbatimModuleSyntax 替代

import Car from "./car"; // 导入的是类型,默认会被移除。应该使用 import type
function buyCar(car: Car) {return car;}

12.downlevelIteration

是否开启对 iterator 降级处理,默认在低版本中直接转化成索引遍历

let arr = [1, 2, 3];
for (let key of arr) {console.log(arr);
}

13.sourceRoot

在 debugger 时,用于定义我们的源文件的根目录。

14.mapRoot

在 debugger 时,用于定义我们的 source map 文件的根目录。

15.inlineSources

增加 sourcesContent,压缩后依然可以找到对应的源代码

16.emitBOM

生成 BOM 头

17.newLine

换行方式 crlf(Carriage Return Line Feed)widows 系统的换行符。lf(Line Feed)Linux 系统的换行方式

18.stripInternal

是否禁止 JSDoc 注释中带有 @internal 的代码发出类型声明

/**
 * @internal
 */
const a = "abc";
export default a;

19.noEmitHelpers

在开启时源码中仍然会使用这些辅助函数,但是不存在从 tslib 中导入的过程,同时需要将 importHelpers 关闭。

export function merge(o1: object, o2: object) {return { ...o1, ...o2};
}

20.noEmitOnError

构建过程中有错误产生会阻止写入

21.preserveConstEnums

让常量枚举也转化成对象输出

22.declarationDir

指定声明文件输出的目录

23.preserveValueImports

保留所有值导入,不进行移除。(未用到也进行保留, 已经废弃) , 同importsNotUsedAsValues

输出相关
declaration 是否产生声明文件
declarationMap 为声明文件也生成 source map,通过 .d.ts 映射到 .ts 文件
emitDeclarationOnly 仅生成 .d.ts 文件,不生成 .js 文件
sourceMap 创建 js 对应的 .map 文件
outFile 将所有结果打包到一个文件中,仅支持 amdsystem模块
outDir 将所有生成的文件发射到此目录中
removeComments 移除 ts 文件内的注释
noEmit 在编译过程中不生成文件,但是编译过程中会进行类型检测。
importHelpers tslib 中引入辅助函数解析高版本语法 {...obj}
importsNotUsedAsValues 是否保留导入后未使用的导入值
downlevelIteration 是否开启对 iterator 降级处理,默认在低版本中直接转化成索引遍历
sourceRoot 在 debugger 时,用于定义我们的源文件的根目录。
mapRoot 在 debugger 时,用于定义我们的 source map 文件的根目录。
inlineSourceMap 内嵌 sourcemap,不能与 sourceMap 属性连用
inlineSources 内链 sourcesContent 属性,压缩后依然可以找到对应的源代码
emitBOM 生成 BOM 头
newLine 换行方式 crlf(Carriage Return Line Feed)widows 系统的换行符。lf(Line Feed)Linux 系统的换行方式
stripInternal 是否禁止 JSDoc 注释中带有 @internal 的代码发出声明
noEmitHelpers 不从 tslib 中导入辅助函数
noEmitOnError 构建过程中有错误产生会阻止写入
preserveConstEnums 让常量枚举也转化成对象输出
declarationDir 指定声明文件输出的目录
preserveValueImports 保留所有值导入,不进行移除。(未用到也进行保留, 已经废弃)

5.Interop Constraints 互操作约束

1.isolatedModules

隔离模块,重导出一个类型需要使用export type

2.verbatimModuleSyntax

取代 isolatedModules、preserveValueImports、importsNotUsedAsValues。import type 就删除,import就留下。

互操作约束
isolatedModules 隔离模块,文件中需要包含 importexport,导入类型需要使用import type 进行导入
verbatimModuleSyntax 取代 isolatedModules、preserveValueImports、importsNotUsedAsValues
allowSyntheticDefaultImports 解决 ES Module 和 CommonJS 之间的兼容性问题。模拟默认导出。
esModuleInterop 解决 ES Module 和 CommonJS 之间的兼容性问题。可以支持import React from 'react'。会自动开启allowSyntheticDefaultImports
preserveSymlinks 不把符号链接解析为真实路径
forceConsistentCasingInFileNames 强制文件名使用时大小写一致

3.allowSyntheticDefaultImports

解决 ES Module 和 CommonJS 之间的兼容性问题。(输出成module:commonjs

function sum(a: number, b: number) {return a + b;}
export = sum;
import sum from "./sum"; // es6 方式导入

兼容模块间转换,模拟 commonjs 默认导出。

4.esModuleInterop

默认开启,解决 ES Module 和 CommonJS 之间的兼容性 (.default) 问题。可以支持import React from 'react'。会自动开启allowSyntheticDefaultImports

5.preserveSymlinks

是否禁用将符号链接解析为其真实路径(开启后等价于 webpack.resolve.symlinks 为 false)。webpack 中大多数情况下采用symlinks:true(Webpack 会按照符号链接的实际位置来解析模块,这是通常的行为。)

6.forceConsistentCasingInFileNames

强制文件名使用时大小写一致

6.Type Checking 类型检测

1.strict

设置为 true 会启用全部类型检测选项,同时也可以指定单独关闭某个具体的类型检测的选项

2.noImplicitAny

为具有隐含“any”类型的表达式和声明启用错误报.

TypeScript 进阶

3.strictNullChecks

开启此选项让 typescript 执行严格的 null 检查

TypeScript 进阶

4.strictFunctionTypes

开启后支持函数参数的双向协变

TypeScript 进阶

5.strictBindCallApply

请检查“bind”、“call”和“apply”方法的参数是否与原始函数匹配。

TypeScript 进阶

6.strictPropertyInitialization

检查构造函数中已声明但未设置的类属性。

TypeScript 进阶

7.noImplicitThis

当“this”的类型为“any”时,报错。

TypeScript 进阶

8.useUnknownInCatchVariables

将 catch 变量默认为“unknown”,而不是“any”。

TypeScript 进阶

9.alwaysStrict

确保输出文件始终带有“use strict”

10.noUnusedLocals

当 ts 发现未使用的局部变量时, 会给出一个编译时错误

TypeScript 进阶

11.noUnusedParameters

当 ts 发现参数未使用时, 会给出一个编译时错误

TypeScript 进阶

12.exactOptionalPropertyTypes

默认值为 false,将可选属性类型解释为写入,而不是添加“未定义”。在初始化时可以留空为undefined, 但是不能被手动设置为undefined

TypeScript 进阶

13.noImplicitReturns

默认值为 false,开启这个选项,所有分支都要有 return。

TypeScript 进阶

14.noFallthroughCasesInSwitch

默认值为 false,开启这个选项,每个 switch 中的 case 都要有 break;

TypeScript 进阶

15.noUncheckedIndexedAccess

默认值为 false,开启这个选项,给索引签名语法声明的属性补上一个 undefined 类型

TypeScript 进阶

16.noImplicitOverride

默认值为 false,开启这个选项,保证子类重写基类的方法时, 必须在方法前加上 override 关键词

TypeScript 进阶

17.noPropertyAccessFromIndexSignature

默认值为 false,开启这个选项,禁止通过访问常规属性的方法来访问索引签名声明的属性。

TypeScript 进阶

18.allowUnusedLabels

默认值为 false,开启这个选项后,允许没有使用的 label

TypeScript 进阶

19.allowUnreachableCode

默认值为 false,开启这个选项后,则允许出现无法触达的代码

TypeScript 进阶

类型检查
strict 启用所有严格类型检测选项
noImplicitAny 关闭后,没有指定参数类型时,默认推导为 any
strictNullChecks 关闭后,null 和 undefiend 将会成为任何类型的子类型
strictFunctionTypes 关闭后,参数变为双向协变
strictBindCallApply 关闭后,不检测 call、bind、apply 传递的参数。
strictPropertyInitialization 关闭后,函数声明属性无需初始化操作。
noImplicitThis 关闭后,this 默认推导为 any
useUnknownInCatchVariables 关闭后,catch 中的 error 类型会变为 any。
alwaysStrict 关闭后,不使用严格模式
noUnusedLocals 关闭后,允许声明未使用的变量
noUnusedParameters 关闭后,允许声明未使用的参数
exactOptionalPropertyTypes 开启后,进行严格可选属性检测,不能赋予 undefined
noImplicitReturns 开启后,要求所有路径都需要有返回值。
noFallthroughCasesInSwitch 开启后,switch、case 中不能存在连续执行的情况。
noUncheckedIndexedAccess 任意接口中访问不存在的属性会在尾部添加 undefiend 类型
noImplicitOverride 增添 override 关键字,才可以覆盖父类的方法
noPropertyAccessFromIndexSignature 不允许访问任意接口中不存在的属性
allowUnusedLabels 是否允许未使用的 label 标签
allowUnreachableCode 是否允许无法执行到的代码

7.Completeness 完整性

完整性
skipLibCheck 跳过类库检测,不检测内置声明文件及第三方声明文件。
skipDefaultLibCheck 跳过 TS 库中内置类库检测。

8.Projects 项目

1.incremental

incremental 配置将启用增量构建,在每次编译时首先 diff 出发生变更的文件,仅对这些文件进行构建,然后将新的编译信息通过 .tsbuildinfo 存储起来。

2.tsBuildInfoFile

控制这些编译信息文件的输出位置。

3.composite

在 Project References 的被引用子项目 tsconfig.json 中必须为启用状态。并且在子项目中必须启用 declaration,必须通过 files 或 includes 声明子项目内需要包含的文件等。

项目相关
incremental 启用增量构建,当使用–watch 的时候可以配合开启
composite 被 references 引用的 tsconfig.json 必须标识为 true
tsBuildInfoFile 增量构建文件的存储路径
disableSourceOfProjectReferenceRedirect 在引用复合项目时首选源文件而不是声明文件。
disableSolutionSearching 编辑时,选择不检查多项目引用的项目。
disableReferencedProjectLoad 禁用引用项目加载

9. 其他

1.files、include 与 exclude

  • 使用 files 我们可以描述本次包含的所有文件,每个值都需要是完整的文件路径,适合在小型项目时使用。
{"include": ["src/**/*", "utils/*.ts"],
  "exclude": ["src/file-excluded", "/**/*.test.ts", "/**/*.e2e.ts"]
}

exclude 只能剔除已经被 include 包含的文件

2.extends

tsconfig.base.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

tsconfig.json

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {"outDir": "./dist"}
}

3.references

可以将整个工程拆分成多个部分,我们可以定义这些部分的引用关系,为它们使用独立的 tsconfig 配置。

  • root

    • index.ts

      import user from "../user";
      console.log(user());
      
    • tsconfig.json

      {
        "extends": "../tsconfig.json",
        "compilerOptions": {
          "target": "ES2015",
          "baseUrl": ".",
          "outDir": "../dist/root"
        },
        "include": ["./**/*.ts"],
        "references": [
          {"path": "../user"}
        ]
      }
      
  • user

    • index.ts

      export default function () {return "get user";}
      
    • tsconfig.json

      {
        "extends": "../tsconfig.json",
        "compilerOptions": {
          "composite": true,
          "target": "ES5",
          "module": "NodeNext",
          "baseUrl": ".",
          "outDir": "../dist/user"
        },
        "include": ["./**/*.ts"]
      }
      
  • tsconfig.json

    {
      "compilerOptions": {
        "declaration": true,
        "module": "NodeNext",
        "moduleResolution": "NodeNext"
      }
    }
    
tsc --build 

4.watchOptions

监听选项, 一般不进行配置

"watchOptions": {
  // 如何监听文件 使用操作系统的原生事件来进行监听
  "watchFile": "useFsEvents",
  // 如何监听目录
  "watchDirectory": "useFsEvents",
  // 对变更不频繁的文件,检查频率降低
  "fallbackPolling": "dynamicPriority",
  "synchronousWatchDirectory": true,
  "excludeDirectories": ["**/node_modules", "_build"],
  "excludeFiles": ["build/fileWhichChangesOften.ts"] // 减少更新范围
}

原文地址: TypeScript 进阶

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-10-07发表,共计76717字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)