TypeScript进阶

Typescript进阶

基础知识

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

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

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

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

什么是 Typescript

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

img

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

环境配置

全局编译 TS 文件

全局安装typescriptTS进行编译

npm install typescript -g
tsc --init # 生成tsconfig.json
tsc # 可以将ts文件编译成js文件
tsc --watch # 监控ts文件变化生成js文件
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 {}进行模块隔离

image-20240803170002284

布尔、数字、字符串类型
let name: string = "hs"; // 全局也有name属性,需要采用模块化解决冲突问题
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: Array<number | string> = [1, "2", 3]; // 后面讲泛型的时候 详细说为什么可以这样写
元组类型

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

let tuple1: [string, number, boolean] = ["hh", 30, true];
tuple[3]; // 长度为 "3" 的元组类型 "[string, number, boolean]" 在索引 "3" 处没有元素。
let tuple2: [name: string, age: number, handsome?: boolean] = ["hh", 30, true]; // 具名元祖
let tuple3: [string, number, boolean] = ["hh", 30, true];
tuple3.push("回龙观"); // ✅ 像元组中增加数据,只能增加元组中存放的类型,但是为了安全依然无法取到新增的属性
// tuple3.push({ address: "回龙观" }); // ❎

let tuple4: readonly [string, number, boolean] = ["hh", 30, true];
// 仅读元祖,不能修改,同时会禁用掉修改数组的相关方法

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

枚举类型

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

enum USER_ROLE {
  USER, // 默认从0开始
  ADMIN,
  MANAGER,
}
// {0: "USER", 1: "ADMIN", 2: "MANAGER", USER: 0, ADMIN: 1, MANAGER: 2}

可以枚举,也可以反举

// 编译后的结果
(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, // 2
}

常量枚举

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

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

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

let u1: undefined = undefined;
let n1: null = null; // 默认情况下 只能null给null , undefiend给undefiend
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 {
  //   throw new Error();
  while (true) {}
}
let a: never = fn(); // never只能赋予给never
let b: number = a; // never是任何类型的子类型,可以赋值给任何类型

never 实现完整性保护

function validate(type: never) {} // 类型“boolean”的参数不能赋给类型“never”的参数。
function getResult(strOrNumOrBool: string | number | boolean) {
  if (typeof strOrNumOrBool === "string") {
    return strOrNumOrBool.split("");
  } else if (typeof strOrNumOrBool === "number") {
    return strOrNumOrBool.toFixed(2);
  }
  // 能将类型“boolean”分配给类型“never”。
  validate(strOrNumOrBool);
}

联合类型自动去除 never

let noNever: string | number | boolean | never = 1; // never自动过滤
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); // 此条件将始终返回 "false",因为类型 "typeof s11" 和 "typeof s12" 没有重叠
BigInt 类型
const num1 = Number.MAX_SAFE_INTEGER + 1;
const num2 = Number.MAX_SAFE_INTEGER + 2;
console.log(num1 == num2); // true

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; // 类型为any
name = "hswen";
name = 30;

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

let name = "hswen"; // name被推导为字符串类型
name = 30;

联合类型

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

let name: string | number; // 联合类型
console.log(name.toString()); // 公共方法
name = 30;
console.log(name.toFixed(2)); // number方法
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); // JS中链判断运算符
    ele!.style.color = "red"; // TS中非空断言ele元素一定有值
    
    • 可选链操作符 ?. 在访问对象的属性或方法时,先检查目标对象及其属性是否存在。
    • 空值合并操作符 ?? ,当左侧的表达式结果为 nullundefined 时,会返回右侧的值。
  • 类型断言

    let name: string | number;
    (name! as number).toFixed(2); // 强制
    (<number>name!).toFixed(2);
    
    name as boolean; // 错误 类型 "string | number" 到类型 "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默认也是公开的
      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); // 属性“name”受保护,只能在类“Animal”及其子类中访问。
    
  • 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']) // 可以访问私有属性,绕过ts检测
    
  • readonly修饰符 (仅读修饰符)

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

    class Animal {
      constructor(public readonly name: string, public age: number) {
        this.name = "init";
        this.age = age;
      }
      changeName(name: string) {
        this.name = name; // 仅读属性只能在constructor中被赋值
      }
    }
    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指代的是父类的原型
    super.say("猫猫叫");
  }
  static getType() {
    // 静态方法中的super指代的是父类
    return super.getType();
  }
}
let cat = new Cat();
console.log(Cat.getType());

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

class Animal {
  say(message: string): void {
    // 这里的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 static type = '哺乳动物' // “static”修饰符不能与“abstract”修饰符一起使用。

  // 可以在父类中定义抽象方法,子类必须要实现
  abstract eat: () => void; // 实例方法eat
  abstract play(): void; // 原型方法play
  // 提供的真实存在的方法
  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 = () => {
  // 这里需要使用const进行声明,可能是因为:防止fn被重新赋值,因为用let修改了值,可能属性就不存在了
  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, // 如何增添这个a属性呢?
};
  • 方案 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; // name | age
type PropTypeValueUnion = IPerson2[PropTypeUnion]; // string | number
类接口

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

interface Speakable {
  name: string;
  speak(): void;
}
// 这里不区分是实例的方法还是原型的方法
interface ChineseSpeakable {
  // speakChinese:()=>void
  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;
}
// type IClazz = new ()=> 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 Clazz<T> {
  new (name: string): T;
}
function createClass<T>(target: Clazz<T>, 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); // 3 => T => number
  • 多个泛型
function swap<T, 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 ICallback = <T>(item: T, index: number) => void; ❎错误写法,这样写意味着调用函数的时候确定泛型
type ICallback<T> = (item: T, index: number) => void;
type IForEach = <T>(arr: T[], callback: ICallback<T>) => void;

const forEach: IForEach = (arr, callback) => {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i); // ts 类型检测 此时不会执行代码。
  }
};
forEach([1, 2, "a", "b"], function (item) {
  console.log(item);
});

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

默认泛型

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

type Union<T = string> = number | T;
const u1: Union = "abc";
const u2: Union<boolean> = true;

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

泛型约束

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

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

案例 1:

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

案例 2:

interface IWithLength {
  length: number;
}
function getLen<T 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 ApiResponse<T = any> {
  code: number;
  data: T;
  message?: string;
}

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

通泛型坑位,来占位置

interface LoginRes {
  // 登录接口的返回值
  token: string;
  roles: number[];
}

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

创建实例时提供类型

class MyArray<T> {
  // T => number
  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 MyArray<number>(); // 没有传递类型,默认类型为unknown
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; // address 内部也会进行交叉类型
type POS = P1P2["address"]["pos"]; // never = string & number
  • 交叉类型
function mixin<T, 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); // unknown
  }
}
// 类型断言后使用
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; // null
    type inter = any & null; // any
    

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

  • keyof unknown 是 never

    type key = keyof unknown; // never
    // type key = keyof any; // string | number | symbol
    

条件类型

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

条件类型基本使用

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

type ResStatusMessage<S extends number> = S extends 200 | 201 | 204
  ? "success"
  : "fail";
type Message = ResStatusMessage<300>; // 传入要判断的类型
type Conditional<T, C> = T extends C ? true : false;
type R1 = Conditional<"hswen", string>; // true
type R2 = Conditional<"hswen", number>; // false 条件也可以通过泛型传入
interface Fish {
  name: "鱼";
}
interface Water {
  type: "水";
}
interface Bird {
  name: "鸟";
}
interface Sky {
  type: "天空";
}
type Condition<T> = T extends Fish ? Water : Sky; // 类型相同也可以使用extends
let con1: Condition<Fish> = { type: "水" };
多条件类
type FormatReturnType<T> = T extends string // 可以编写多条件类型
  ? string
  : T extends number
  ? number
  : never;

function sum<T extends string | number>(x: T, y: T): FormatReturnType<T> {
  // 泛型不能做数学运算
  return x + (y as any);
}
sum("abc", "abc"); // string
sum(123, 123); // number

类型兼容性问题

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

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

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

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

联合类型的兼容性

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

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

// so~~~
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; // true
type R8 = number extends Number ? true : false; // true
type R9 = object extends Object ? true : false; // true
type R10 = String extends Object ? true : false; // true

// so~~~
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; // true
type R12 = Object extends unknown ? true : false; // true

// so~~~
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; // true
type R14 = undefined extends undefined ? true : false; // true
type R15 = null extends null ? true : false; // true
type R16 = undefined extends void ? true : false; // true

never 为最底端类型。

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

类型层级

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

  • never < 字面量类型
  • 字面量类型 < 字面量类型的联合类型
  • 原始类型 < 原始类型的联合类型
  • 原始类型 < 装箱类型 < Object 类型
  • Object < any | unknown
unknown & any 特殊情况
type R17 = unknown extends 1 ? true : false; // 不能赋予给除unknown之外的类型
type R18 = any extends 1 ? true : false; // boolean
type R19 = any extends any ? true : false; // 条件是 any,依然会进行判断

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

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

// 鸭子类型检测,可以看出对象是基于{}扩展出来的
type R22 = Object extends {} ? true : false; // true
type R23 = object extends {} ? true : false; // true

// 以下两种情况均默认成立
type R24 = Object extends object ? true : false; // true
type R25 = object extends Object ? true : false; // true

条件类型与映射类型

条件类型分发

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

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

type c1 = Condition2<Fish>;
type c2 = Condition2<Bird>;
type c = c1 | c2;
禁用分发

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

// type unionAssets<T, U> = T extends U ? true : false;
type R1 = unionAssets<1 | 2, 1 | 2 | 3>; // true 看似正常
type R2 = unionAssets<1 | 2, 1>; // boolean (开启分发类型结果为boolean)

// 禁用分发
type unionAssets<T, U> = [T] extends [U] ? true : false;
type NoDistribute<T> = T & {}; // 这种情况会返回一个新类型,从而阻止分发
type unionAssets<T, U> = NoDistribute<T> extends U ? true : false;
特殊问题

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

type isNever1<T> = T extends never ? true : false;
type isNever2<T> = [T] extends [never] ? true : false; // 包裹后不在是never
type R4 = isNever1<never>; // 返回never
type R5 = isNever2<never>; // 返回true

内置条件类型

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

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

type Complement<T, U extends T> = T extends U ? never : T;
type MyComplement = Complement<"1" | "2" | "3", "1" | "2">; // 补集
NoNullable 非空检测
type NonNullable<T> = T extends null | undefined ? never : T;
type NonNullable<T> = 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 ReturnType<T> = T extends (...args: any) => infer R ? R : never;
    type MyReturn = ReturnType<typeof getUser>;
    
  • Parameters 参数类型

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

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

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

类型交换

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

递归推断

type PromiseVal<T> = T extends Promise<infer V> ? PromiseVal<V> : T;
type PromiseResult = PromiseVal<Promise<Promise<number>>>; // number

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

type ElementOf<T> = T extends Array<infer 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 Compute<T> = {
  // 映射类型   索引类型查询   索引类型访问
  [K in keyof T]: T[K];
};
type A1A2 = Compute<A1 & A2>; // {name:string,age:number}
Partial 转化可选属性
interface Company {
  num: number;
}
interface Person {
  name: string;
  age: string;
  company: Company;
}
// type Partial<T> = { [K in keyof T]?: T[K] }; 实现原理
type PartialPerson = Partial<Person>;

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

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

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

Required
type PartialPerson = Partial<Person>;
type Required<T> = { [K in keyof T]-?: T[K] };
type RequiredPerson = Required<PartialPerson>;

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

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

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

type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 所有属性变成可变属性
type MutablePerson = Mutable<ReadonlyPerson>;

结构类型

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

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

Omit 忽略属性
let person = {
  name: "hs",
  age: 11,
  address: "回龙观",
};
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type OmitAddress = Omit<typeof person, "address">;

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

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

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

record 通常用来代替 object 。

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

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

function map<T extends keyof any, K, U>(
  obj: Record<T, K>,
  callback: (item: K, key: T) => U
) {
  let result = {} as Record<T, 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; // 字符串中具备toString()方法,所以可以进行兼容

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 Func<T> = (item: T, index: number) => void;
    function forEach<T>(arr: T[], cb: Func<T>) {
      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(); // 可以看成ClassB是继承于ClassA的子类,子类赋予给父类兼容

这里要注意的是,只要有 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(); // 不能将类型“ClassB”分配给类型“ClassA”。这也做到了模拟标称类型系统

结构化类型导致的问题

type BTC = number; // 无法区分两个类型
type USDT = number;

let btc: BTC = 1000;
let usdt: USDT = 1000;
// 要求传入btc
function getCount(count: BTC) {
  return count as BTC;
}
let count = getCount(usdt); // 实际传入usdt
type Nominal<T, U extends string> = T & { __tag: U };
type BTC = Nominal<number, "btc">;
type USDT = Nominal<number, "usdt">; // 标称类型

let btc: BTC = 1000 as BTC;
let usdt: USDT = 1000 as USDT;
function getCount(count: BTC) {
  // 获取BTC的数量
  return count;
}
let count = getCount(usdt); // 报错:无法传入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()); // 如果传递的是子类型,在使用的时候无法使用多出来的属性
  // ins是Child类型,我可以将Grandson类型传入。用的时候我只会调用Child类型的方法。因为安全所以兼容
}
fn((instance: Parent) => {
  // instance.sleep() 这个不安全。因为如果传递的是Child 他不具备。
  // 但是如果这里标识Parent 是可以的。因为调用instance.house() 是安全的。
  return new Grandson();
});

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

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

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

由此可得:

type Arg<T> = (arg: T) => void;
type Return<T> = (arg: any) => T;
type ArgReturn = Arg<Parent> extends Arg<Child> ? true : false; // 基于函数参数的逆变
type ReturnReturn = Return<Grandson> extends Return<Child> ? true : false; // 返回值是协变的

逆变带来的问题:

interface Array<T> {
  // concat: (...args: T[]) => T[]; // 严格检参数测逆 Child 无法 赋予给 Parent
  concat(...args: T[]): T[]; // 不进行参数逆变检测
  [key: number]: T;
}
let parentArr!: Array<Parent>;
let childArr!: Array<Child>;

parentArr = childArr; // 子应该可以赋予给父的~~~
泛型的兼容性
interface IT<T> {}
let obj1: IT<string>;
let obj2!: IT<number>;
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<T>(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<T extends object, K extends keyof T> = Partial<
  Pick<T, K>
> &
  Omit<T, K>;

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

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

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

// 3) 通过联合类型挑选出所需的类型
type PickKeysByValue<T extends object, U> = Pick<
  T,
  ExtractKeysByValueType<T, U>
>;

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

type t3 = OmitKeysByValue<Person, string>;
// 重映射实现
type PickKeysByValue<T extends object, U> = {
  [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<T, U> = { [K in Exclude<keyof T, keyof U>]?: never };

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

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

交集

type ObjectInter<T extends object, U extends object> = Pick<
  T,
  Extract<keyof T, keyof U>
>;

差集

type ObjectDiff<T extends object, U extends object> = Pick<
  T,
  Exclude<keyof T, keyof U>
>;

补集

// T多U少
type ObjectComp<T extends U, U extends object> = Pick<
  T,
  Exclude<keyof T, keyof U>
>;

重写

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

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

借助 Parameters 类型简化

type LastParameter<T extends (...args: any[]) => any> = Parameters<T> extends [
  ...any,
  infer Q
]
  ? Q
  : never;

模块及命名空间使用

模块和命名空间

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

模块

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

// a.ts导出
export default "hh";

// index.ts导入
import name from "./a";

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

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

TS 模块语法

// a.ts导出
export = "hh";

// index.ts导入
import name = require("./a"); // 也可以采用ES的方式导入,同时也可以打包成Commonjs或者AMD模块
命名空间

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

  • 命名空间的使用
// a.ts导出
export namespace Zoo {
  export class Dog {
    eat() {
      console.log("zoo dog");
    }
  }
}
export namespace Home {
  export class Dog {
    eat() {
      console.log("home dog");
    }
  }
}
// index.ts导入
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;
}

// $(".box").height(100).width(100);
// $.fn.extend({});
声明模块
// declare.d.ts
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;
}

// index.ts
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

// lodash.d.ts
/// <reference path="./lodash_a.d.ts" />
export = _; // 将_当做模块导出
export as namespace _; // 将这个模块作为全局变量使用,不需要导入(在不是作用域的文件中可以直接使用,umd 模块)

declare namespace _ {
  function a(): void;
  function b(): void;
  function c(): void;
}
// lodash_a.d.ts
import _ = require("./lodash");
declare module "./lodash" {
  // 对模块进行扩展
  function x(): void;
  function y(): void;
  function z(): void;
}

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

/// <reference path="./lodash_a.d.ts" />
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;
  }
}
三斜线指令

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

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

/// <reference path="./lodash_a.d.ts" /> // 依赖的某个声明
/// <reference types="node" /> // 依赖的某个包
/// <reference lib="dom" /> // 依赖的内置声明

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

扩展全局变量类型

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

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

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

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

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

// @ts-expect-error
const age: number = 30;
ts-nocheck

忽略整个文件的类型检测

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

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

// @ts-check
/**
 @param {string} a
 @param {string} b
 @returns {string}
*/
function getType(a, b) {
  return a + b;
}
getType("1", "2");

/** @type {string} */
const age = 30;

类型体操

基于字符串

CapitalizeString

首字母大写

// 默认情况下,同时进行推断,左边只有一个字母
export type CapitalizeString<T> = T extends `${infer L}${infer R}`
  ? `${Capitalize<L>}${R}` // 左边大写 + 右边剩下的
  : T; // 不是字符串则直接返回

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

type a1 = CapitalizeString<"handler">; // Handler
type a2 = CapitalizeString<"parent">; // Parent
type a3 = CapitalizeString<233>; // 233
FirstChar

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

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

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

type A = FirstChar<"BFE">; // 'B'
type B = FirstChar<"dev">; // 'd'
type C = FirstChar<"">; // never
LastChar

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

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

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

type A = LastChar<"BFE">; // 'E'
type B = LastChar<"dev">; // 'v'
type C = LastChar<"">; // never
StringToTuple

字符串转换为元组类型

export type StringToTuple<
  T,
  F extends any[] = []
> = T extends `${infer L}${infer R}` ? StringToTuple<R, [...F, L]> : F;

type A = StringToTuple<"BFE.dev">; // ['B', 'F', 'E', '.', 'd', 'e','v']
type B = StringToTuple<"">; // []
TupleToString

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

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

// ---------------------------------
type A = TupleToString<["a", "b", "c"]>; // 'abc'
type B = TupleToString<["a"]>; // 'a'
type C = TupleToString<[]>; // ''
RepeatString

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

export type RepeatString<
  T extends string, // 要循环的字符串
  C, // 循环的次数
  A extends any[] = [], // 采用数组记录循环的次数
  F extends string = "" // 最终结果

  // 如果满足长度返回最终结果,不满足则累加数组长度,并且拼接最终结果
> = C extends A["length"] ? F : RepeatString<T, C, [...A, T], `${F}${T}`>;
// ---------------------------------

type A = RepeatString<"a", 3>; // 'aaa'
type B = RepeatString<"a", 0>; // ''
SplitString

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

export type SplitString<
  T, // 要拆分的内容
  S extends string, // 分隔符
  A extends any[] = [] // 存放拆分后的结果
> = T extends `${infer L}${S}${infer R}`
  ? SplitString<R, S, [...A, L]> // 递归拆分右边,并且将左边放到数组中
  : [...A, T]; // 不包含则直接将T 放到数组中

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

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

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

export type LengthOfString<
  T,
  A extends any[] = [] // 用于计算字符串的长度
> = T extends `${infer L}${infer R}`
  ? LengthOfString<R, [...A, L]>
  : A["length"];

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

type A = LengthOfString<"BFE.dev">; // 7
type B = LengthOfString<"">; // 0
KebabCase

驼峰命名转横杠命名

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

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

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

type a1 = KebabCase<"HandleOpenFlag">; // handle-open-flag
type a2 = KebabCase<"OpenFlag">; // open-flag
CamelCase

横杠命名转化为驼峰命名

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

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

type a1 = CamelCase<"handle-open-flag">; // HandleOpenFlag
type a2 = CamelCase<"open-flag">; // OpenFlag
ObjectAccessPaths

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

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

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

function createI18n<Schema>(
  schema: Schema
): (path: ObjectAccessPaths<Schema>) => 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 string, C extends string> = T extends ""
  ? C extends ""
    ? true
    : false
  : T extends `${infer L}${C}${infer R}` // 可以实现 startsWith、endsWith
  ? true
  : false;

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

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

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

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

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

type a1 = Replace<"ha ha ha", "ha", "he">;
type a2 = Replace<"hh", "hh", "hswen">;
type a3 = Replace<"a", "", "hswen">;
type a4 = Replace<"", "", "hswen">;
ComponentEmitsType

定义组件的监听事件类型

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

// 实现 ComponentEmitsType<Emits> 类型,将
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<T> = {
  [K in keyof T as `on${CamelCase<K & string>}`]?: T[K] extends (
    ...args: infer R
  ) => any
    ? (...args: R) => void
    : T[K];
};

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

基于数组

LengthOfTuple

计算元组类型的长度

export type LengthOfTuple<T extends any[]> = T["length"];

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

type A = LengthOfTuple<["B", "F", "E"]>; // 3
type B = LengthOfTuple<[]>; // 0
FirstItem

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

export type FirstItem<T extends any[]> = T[0];

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

type A = FirstItem<[string, number, boolean]>; // string
type B = FirstItem<["B", "F", "E"]>; // 'B'
LastItem

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

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

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

type A = LastItem<[string, number, boolean]>; // boolean
type B = LastItem<["B", "F", "E"]>; // 'E'
type C = LastItem<[]>; // never
Shift

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

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

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

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

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

export type Push<T extends any[], I> = [...T, I];

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

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

反转元组

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

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

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

拍平元组

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

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

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

export type Repeat<T, C, F extends any[] = []> = C extends F["length"]
  ? F
  : Repeat<T, C, [...F, T]>;

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

type A = Repeat<number, 3>; // [number, number, number]
type B = Repeat<string, 2>; // [string, string]
type C = Repeat<1, 1>; // [1]
type D = Repeat<0, 0>; // []
Filter

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

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

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

type A = Filter<[1, "BFE", 2, true, "dev"], number>; // [1, 2]
type B = Filter<[1, "BFE", 2, true, "dev"], string>; // ['BFE', 'dev']
type C = Filter<[1, "BFE", 2, any, "dev"], string>; // ['BFE', any, 'dev']
FindIndex

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

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

export type FindIndex<T extends any[], A, F extends any[] = []> = T extends [
  infer L,
  ...infer R
]
  ? IsEqual<L, A, F["length"], FindIndex<R, A, [...F, null]>>
  : never;

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

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

元组类型转换为枚举类型

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

type TupleToEnum<T extends any[], C = false> = {
  [K in T[number]]: C extends true ? FindIndex<T, K> : K;
};

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

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

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

截取元组中的部分元素

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

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

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

删除并且替换部分元素

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

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

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

基于结构

OptionalKeys

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

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

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

type a1 = OptionalKeys<{
  foo: number | undefined;
  bar?: string;
  flag: boolean;
}>; // bar
type a2 = OptionalKeys<{ foo: number; bar?: string }>; // bar
type a3 = OptionalKeys<{ foo: number; flag: boolean }>; // never
type a4 = OptionalKeys<{ foo?: number; flag?: boolean }>; // foo|flag
type a5 = OptionalKeys<{}>; // never
PickOptional

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

export type PickOptional<T> = Pick<T, OptionalKeys<T>>;

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

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

export type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;

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

type a1 = RequiredKeys<{
  foo: number | undefined;
  bar?: string;
  flag: boolean;
}>; // foo|flag
type a2 = RequiredKeys<{ foo: number; bar?: string }>; // foo
type a3 = RequiredKeys<{ foo: number; flag: boolean }>; // foo|flag
type a4 = RequiredKeys<{ foo?: number; flag?: boolean }>; // never
type a5 = RequiredKeys<{}>; // never
PickRequired

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

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

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

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

判断是否为 never 类型

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

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

type A = IsNever<never>; // true
type B = IsNever<string>; // false
type C = IsNever<undefined>; // false
type D = IsNever<any>; // false
IsEmptyType

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

export type IsEmptyType<T> = [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<string>; // false
type B = IsEmptyType<{ a: 3 }>; // false
type C = IsEmptyType<{}>; // true
type D = IsEmptyType<any>; // false
type E = IsEmptyType<object>; // false
type F = IsEmptyType<Object>; // false
type G = IsEmptyType<unknown>; // false
IsAny
type IsAny<T> = 0 extends 1 & T ? true : false;

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

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

type A = IsAny<string>; // false
type B = IsAny<any>; // true
type C = IsAny<unknown>; // false
type D = IsAny<never>; // false
Redux Connect
type transform<T> = T extends (
  input: Promise<infer U>
) => Promise<Action<infer S>>
  ? (input: U) => Action<S>
  : T extends (aciton: Action<infer U>) => Action<infer S>
  ? (action: U) => Action<S>
  : never;

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

// ----------------------
interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}

interface Action<T> {
  payload?: T;
  type: string;
}

// 这个要求的结果
type Result = {
  asyncMethod<T, U>(input: T): Action<U>;
  syncMethod<T, U>(action: T): Action<U>;
};

// 实现类型Connect,要求 Connect<Module> 的结果为上面的 Result
// 只要函数类型的属性;
// 如果函数是异步函数,要求自动解析出来Promise中的类型;

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

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

逆变参数可以传父亲

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

// 先映射成函数 得到函数的联合类型
// 在extends 推断参数即可
export type UnionToIntersection<T> = (
  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 ToIntersection<T> = T extends [(x: infer U) => any, (x: infer U) => any]
  ? U
  : never;
type t3 = ToIntersection<[(x: T1) => an y, (x: T2) => any]>;

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

// type A = UnionToIntersection<{ a: string } | { b: string } | { c: string }>;
// {a: string} & {b: string} & {c: string}
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 ParamaterType<T> = T extends (value: infer R) => any ? R : never;
type R = ParamaterType<X>;

// 先变成函数的联合类型
type FindUnionOne<T> = IsAny<T> 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;

// 1)先转换成交函数叉类型
// 2)推断函数的参数,利用特性随机返回一个
// 3) 排除boolean类型 boolean 会发生分发

type UnionToTuple<U, Last = FindUnionOne<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];

type a1 = UnionToTuple<1 | 2 | boolean | string>;

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

type a = UnionToTuple<1 | 2 | 3>; // [1,2,3]

模板字符串以及装饰器

模板字符串类型

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

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

// 类型分发机制 1)
type Direction = "left" | "right" | "top" | "bottom";
type AllMargin = `marigin-${Direction}`; // "marigin-left" | "marigin-right" | "marigin-top" | "marigin-bottom"

// 类型分发机制 2)
type IColor = "red" | "yellow" | "green";
type ICount = 100 | 200 | 300;
type BookSKU = `${IColor}-${ICount}`; // "red-100" | "red-200" | "red-300" | "yellow-100" | "yellow-200" | "yellow-300" | "green-100" | "green-200" | "green-300"
通过泛传入类型
type sayHello<T extends string | number | bigint | boolean | null | undefined> =
  `hello, ${T}`; // 泛型要求:string | number | bigint | boolean| null | undefiend

type V1 = sayHello<"hs">; // "Hello, hs"
type V2 = sayHello<30>; // "Hello, 30"
type V3 = sayHello<123n>; // "Hello, 123"
type V4 = sayHello<true>; // "Hello, true"
type V5 = sayHello<null>; // "Hello, null"
type V6 = sayHello<undefined>; // "Hello, undefined"
type v7 = sayHello<string>; // `hello, ${string}`
type v8 = sayHello<number>; // `hello, ${number}`

// 传入类型不会被解析,为所有`hello, `开头的父类型
type isChild = V1 extends v7 ? true : false;
映射类型中使用模板字符串
对key进行重命名
type Person = { name: string; age: number; address: string };
type RenamePerson<T> = {
  [K in keyof T as `re_${K & string}`]: T[K]; // K & string 保证K为string类型
};
let person: RenamePerson<Person> = {
  re_name: "hs",
  re_age: 30,
  re_address: "回龙观",
};
专用工具类型

Uppercase、Lowercase、Capitalize 、Uncapitalize

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

装饰器

装饰器本质就是一个函数,只能在类以及类成员上使用。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 {
    // 通过返回子类的方式对父类进行装饰。 最终会用子类替代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   是否可枚举
    // descriptor.writable     是否可写
    // descriptor.configurable 是否能被删除
    // descriptor.value        原来的值
    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 = "";
  // "target": "ES2015" 可以进行劫持 , ESNext访问时无法劫持
  Object.defineProperty(target, key, {
    // 给原型上添加了个属性
    enumerable: true,
    get() {
      return val.toUpperCase();
    },
    set(newValue) {
      val = newValue;
    },
  });
}

class Animal {
  @ToUpper
  public name: string = "Animal"; // 触发原型属性上的set方法
}
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"; // 触发原型属性上的set方法
  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";
  }
}

// [实例属性、方法(优先执行参数装饰器)、属性访问]、[静态属性、静态方法]、构造函数参数装饰器、类装饰器 (同时使用多个装饰器的执行流程“洋葱模型”)

// 实例方法参数装饰器
// 实例方法装饰器
// 实例属性装饰器
// 属性访问装饰器
// 静态方法参数装饰器
// 静态方法装饰器
// 静态属性装饰器
// 构造函数参数装饰器
// 类装饰器4
// 类装饰器3
// 类装饰器2
// 类装饰器1
  • 方法装饰器,我们通常进行方法执行前后的逻辑注入。
  • 属性、参数装饰器,我们通常只进行信息注册,委托别人处理。

反射元数据 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<T>(key: string, creator: () => T) {
    if (!this.instances.has(key)) {
      this.instances.set(key, creator());
    }
    return this.instances.get(key) as T;
  }
  resolve<T>(key: string): T {
    return this.instances.get(key) as T;
  }
}
const container = new Container();
container.bind<Monitor>("Monitor", () => new Monitor27inch());
container.bind<Host>("Host", () => new AppleHost());
const computer = container.bind<Computer>(
  "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<T>(key: string, creator: () => T) {
    if (!this.instances.has(key)) {
      this.instances.set(key, creator());
    }
    return this.instances.get(key) as T;
  }
  resolve<T>(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");
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";

// 1.定义”传递数据“和”返回数据“的接口
interface Person {
  name: string;
  age: number;
}
let person: Person = { name: "hswen", age: 30 };

// 2.配置请求参数
let requestConfig: AxiosRequestConfig = {
  method: "get",
  url: baseURL + "/get",
  params: person,
};

// 3.发送请求,并且限制接口返回值类型
axios(requestConfig)
  .then((response: AxiosResponse) => {
    return response.data;
  })
  .catch((error: any) => {
    console.log(error);
  });

创建 axios 基本结构

axios/index.ts

class Axios {
  request() {}
}
function createInstance() {
  // 1.创建axios实例
  const context = new Axios();
  // 2.获取request方法,并且绑定this
  const instance = Axios.prototype.request.bind(context);
  return instance;
}

// 我们真实调用的就是axios.request方法
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 AxiosResponse<T = any> {
  data: T;
  status: number;
  statusText: string;
  headers: Record<string, any>;
  config: AxiosRequestConfig;
  request?: XMLHttpRequest;
}

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

编写请求方法

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

用于描述 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<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    // 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。

    // todo...
    return this.dipsatchRequest(config);
  }
  dipsatchRequest<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    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 < 300) {
            let response: AxiosResponse<T> = {
              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<string, any>;
  data?: Record<string, any>;
}
修改发送逻辑
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 < 300) {
      // ...
    } else {
      reject(`Error: Request faild with status code ${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<string, any>;
}
export interface AxiosInstance {
  <T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
  interceptors: {
    request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
}

AxiosInterceptorManager 实现

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

export interface Interceptor<V> {
  onFulfilled?: OnFulfilled<V>;
  onRejected?: OnRejected;
}
class AxiosInterceptorManager<V> {
  public interceptors: Array<Interceptor<V> | null> = [];
  use(onFulfilled?: OnFulfilled<V>, 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<InternalAxiosRequestConfig>(),
    response: new AxiosInterceptorManager<AxiosResponse>(),
  };
}
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<AxiosResponse>
  | Interceptor<InternalAxiosRequestConfig>
)[] = [{ 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<AxiosRequestConfig | AxiosResponse> =
  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<AxiosResponse<T>>;

合并配置

创建默认值对象
// 默认配置
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<string, any>;
  data?: Record<string, any>;
  timeout?: number; // 增加超时时间

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

方法实现

// 默认配置
const defaults: InternalAxiosRequestConfig = {
  method: "get",
  headers: {
    common: {
      accept: "application/json",
    },
  },
  // 请求前执行此方法
  transformRequest: (
    data: Record<string, any>,
    headers: Record<string, any>
  ) => {
    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<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
  // 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。
  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 < 300) {
      // ...
      // 转化响应方法
      if (config.transformResponse) {
        response.data = config.transformResponse(response.data);
      }
      resolve(response);
    }
  }
};

请求终止

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<Person>) => {
    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<Cancel>((resolve) => {
        this.resolve = resolve;
      }),
      // 让这个promise成功,并且传入中断的原因
      cancel: (reason: string) => {
        this.resolve(new Cancel(reason));
      },
    };
  }
}
声明所需类型

axios/types.ts

export interface AxiosInstance {
  <T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
  interceptors: {
    request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
  CancelToken: CancelTokenStatic; // 取消token
  isCancel: typeof isCancel; // 请求是否是被取消
}
export type CancelToken = ReturnType<CancelTokenStatic["source"]>["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<T> = {
  get(): T;
  set(value: T): void;
};
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>;
};
function proxify<T>(obj: T): Proxify<T> {
  let result = {} as Proxify<T>;
  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<T>(proxpProps: Proxify<T>): 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<T = any> {
  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<ResponseData>) => {
        // 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<T = any>(url: string, data: any): Promise<ResponseData<T>> {
    return this.request({
      method: "get",
      url,
      params: data,
    })
      .then((res) => {
        return Promise.resolve(res.data);
      })
      .catch((err) => {
        return Promise.reject(err);
      });
  }

  public post<T = any>(url: string, data: any): Promise<ResponseData<T>> {
    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<{ token: number; username: string }>("/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,支持通过别名的方式进行导入。

image-20230905115904162

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

6.rootDirs

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

image-20230905130946578

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"]

image-20230905134030037

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 向上查找的层级

image-20230905172127213

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 中导出而不是在源码中定义。

image-20230905180702672

需要安装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在编译过程中不生成文件,但是编译过程中会进行类型检测。
importHelperstslib中引入辅助函数解析高版本语法 {...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”类型的表达式和声明启用错误报.

image-20230904173622951

3.strictNullChecks

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

image-20230904180305387

4.strictFunctionTypes

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

image-20230904182230767

5.strictBindCallApply

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

image-20230904182655811

6.strictPropertyInitialization

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

image-20230904182944653

7.noImplicitThis

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

image-20230904183444565

8.useUnknownInCatchVariables

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

image-20230904183634256

9.alwaysStrict

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

10.noUnusedLocals

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

image-20230904200239262

11.noUnusedParameters

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

image-20230904200529109

12.exactOptionalPropertyTypes

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

image-20230904230349202

13.noImplicitReturns

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

image-20230905005057547

14.noFallthroughCasesInSwitch

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

image-20230905005405028

15.noUncheckedIndexedAccess

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

image-20230905005833025

16.noImplicitOverride

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

image-20230905010216331

17.noPropertyAccessFromIndexSignature

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

image-20230905010846558

18.allowUnusedLabels

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

image-20230905011343726

19.allowUnreachableCode

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

image-20230905011714348

类型检查
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"] // 减少更新范围
}
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值