TypeScript 类的使用

进行ES5开发的时候,需要使用函数和原型链实现类和继承。ES6引入了 class关键字,我们可以更加方便地定义和使用类。

作为 JavaScript 的超集,TypeScript 同样支持使用 class 关键字,并且可以对类的属性和方法等进行静态类型检测。

具体的类定义方式如下:

  • 通常使用 class 关键字来定义类。
  • 类内部可以声明各种属性,包括类型声明和初始值设定。
  • 如果没有类型声明,则默认为 any 类型。
  • 属性可以有初始化值。
  • 在 默认的 strictPropertyInitialization 模式下,属性必须初始化,否则编译时会报错。
  • 类可以有自己的构造函数(constructor), 当使用 new 关键字创建实例时,构造函数会被调用。另外,构造函数不需要返回任何值,它默认返回当前创建的实例。
  • 类可以有自己的函数,这些函数称为方法。
  • 示例方法:

    class Persion {
        // 定义属性,需要初始化,否则会报错
        name: string = '张三';
        age: number = 18;
        // 定义方法
        dosomething() {
            console.log(this.name + '在干点啥');
    const p = new Persion(); // 创建实例
    console.log(p.name,p.age); // 访问实例属性
    p.dosomething(); // 调用方法
    

    上面代码,首先使用 class 关键字定义Person 类,为该类定义了name、age 属性并初始化,接着定义 dosomething 方法。然后创建 Person 的对象,以及访问对象中的属性和调用 dosomething 方法。

    注意:在 TypeScript 中定义类的属性没有初始化值会报错。

    上述代码是存在缺陷的,比如创建了多个 Person 对象,每个对象的 name 和 age 初始值都一样。这样显然不符合我们的需求,这时可以将属性初始化的过程放到构造器中,代码如下:

    class Person {
        name: string;
        age: number;
        constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
         // 定义方法
        dosomething() {
            console.log(this.name + '在干点啥');
    const p = new Person('张三', 18); // 创建实例
    console.log(p.name,p.age); // 访问实例属性
    p.dosomething(); // 调用方法
    

    在 JavaScript 中,使用 extends 关键字实现继承,然后在子类中使用 super 访问父类。

    class Person {
        name: string;
        age: number;
        constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
        dosomething() : void {
            console.log(this.name + '在干点啥');
    class Student extends Person {
        phone: number;
        constructor(name: string, age: number, phone: number) {
            super(name, age); // super 调用父类的构造器
            this.phone = phone;
        studying() : void {
            console.log('studying');
    const stu = new Student('张三', 18, 15512345678);
    console.log(stu.name, stu.age); // 访问继承父类的属性
    stu.dosomething(); // 调用继承父类的方法
    

    以上代码,在定义 Student 类时,使用 extends 关键字继承 Person 类,这样 Student 类可以拥有自己的属性和方法,也会继承 Person 的属性和方法。在 Student 构造器中,可以使用 super 关键字调用父类的构造方法,对父类中的属性进行初始化。

    在实现类的继承时,也可以在子类中对父类的方法进行重写。比如,Student 类可以重写 Person 类中的 dosomething 方法,代码如下:

    class Student extends Person {
       ......
        // 重新父类方法
        dosomething(): void {
            console.log('student is doing something');
            super.dosomething(); // 调用父类的方法
    ......
    stu.dosomething(); // 调用子类的方法
    

    从上面代码看出,我们重新了父类的 dosomething 方法, 当我们调用 Student 的 dosomething 方法时,会调用子类 Student 中的 dosomething 方法,而不是直接调用父类 Person 的 dosomething 方法。

    如果想调用父类的 dosomething 方法,需要使用 super 关键字,例如调用 super.dosomething 方法。

    面向对象编程中的三大特性是:封装、继承和多态。维基百科对多态的定义是:多态(polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号表示多个不同的类型。

    维基百科的定义比较抽象,也不太容易理解。转换为好理解的意思就是:不同的数据类型在进行同一个操作时表现出不同的行为,这就是多态的体现。

    示例代码:

    class Animal {
        action(): void {
            console.log('animal action');
    class Dog extends Animal { 
        action(): void {
            console.log('狗在叫');
    class Fish extends Animal {
        action(): void {
            console.log('鱼在游');
    function makeActions(animals: Animal[]): void {
        // animals 是父类的引用,指向子类对象
        animals.forEach(animal => {
            animal.action(); // 调用子类的 action 方法
    makeActions([new Dog(), new Fish()]);
    

    可以看到,继承是多态的前提。在 makeActions 函数中,接收的 animals 数组包含 dog 和 fish 对象,它们都是 Animal 类的子类,即父类的引用指向了子类的对象。

    当调用 animal.action 方法时,实际上调用的是子类的 action 方法。这就是多态的体现,即对不同的数据类型进行同一个操作时,表现出不同的行为。

    成员修饰符

    在 TypeScript中,可以使用三种修饰符来控制类的属性和方法的可见性,分别是 public、
    private 和 protected。

  • public: 默认的修饰符,它表示属性或方法是公有的,可以在类的内部和外部被访问。
  • private: 表示属性或方法是私有的,只能在类的内部被访问,外部无法访问。
  • protected: 表示属性或方法是受保护的,只能在类的内部及其子类中被访问,外部无法访问。
  • 1.private 修饰符

    class Person {
        private name: string = '';
        // 默认是 public 方法
        getName() {
            return this.name;
        setName(name: string) {
            this.name = name;
    const p = new Person();
    // console.log(p.name); // 报错
    console.log(p.getName()); // 正确
    p.setName('张三'); // 正确
    

    2.protected 修饰符

    class Person {
        protected name: string = 'zhangsan';
    class Student extends Person {
        getName() {
            return this.name;
    const stu = new Student();
    // console.log(stu.name); // 报错,属性“name”受保护,只能在类“Person”及其子类中访问。
    console.log(stu.getName()); // zhangsan
    

    可以看出,受保护的 name 属性不能被外界直接访问,但是在自身类和子类中可以被访问。

    如果我们不想外部随意修改内部的某一属性,我们可以给当前属性设置readyonly。

    示例代码:

    type FriendType = {
        name: string
    class Person {
        readonly name: string
        readonly age: number
        readonly friend?: FriendType
        constructor(name: string, age: number,friend?: FriendType) {
            this.name = name;
            this.age = age;
            this.friend = friend;
    const p = new Person('tom',18,{name:'jerry'});
    // p.name = 'zhangsan'; // 报错: cannot assign to 'name' because it is a read-only property.
    console.log(p.name,p.age,p.friend); // 结果:tom 18 { name: 'jerry' }
    // p.friend = {name:'lisi'}; // 报错: cannot assign to 'friend' because it is a read-only property.
    if(p.friend) {
        p.friend.name = 'lisi'; // friend对象的name属性是可以修改的
    console.log(p); // 结果:Person { name: 'tom', age: 18, friend: { name: 'lisi' } }
    

    从上面可以看出,只读属性在外界是不能被修改的,但是可以在构造器中赋值,赋值之后也不以修改。

    如果只读属性是对象类型(如friend),那么对象中的属性是可以修改的。例如,p.friend是不能修改的,但是 p.friend.name 是可以修改的。

    getter/setter

    对于一些私有属性,我们不能直接访问,或者对于某些属性,我们想要监听其获取和设置的过程。这时,可以使用 getter 和 setter 访问器。

    如下示例:

    class Person {
        private _name: string;
        constructor(name: string) {
            this._name = name;
        set name(newName: string){
            this._name = newName;
        get name() {
            return this._name;
    const p = new Person('zhangsan');
    p.name = 'lisi'; // 调用 setter 访问器为 name 设置值
    console.log(p.name); // 调用 getter 访问器获取 name 的值
    

    通过上面代码可以看出,在Person 类中定义 getter 和 setter 访问器,实现对 name 私有属性的获取和存储。

    在 TypeScript 中,可以使用关键字 static 来定义类的静态成员。

    class Student {
        static getupTime: string = '6:30';
        static studying(): void {
            console.log('起床后去学习')
    console.log(Student.getupTime); // 访问静态属性
    Student.studying(); // 调用静态方法
    

    从上面代码可以看出,Student 类添加了静态属性 getupTime 和 静态方法 studying。

    在面向对象编程中,继承和多态是密切相关的。为了定义通用的调用接口,我们通常会让调用者传入父类,通过多态实现更加灵活的调用方式。父类本身可能不需要对某些方法进行具体实现,这时可以将这些方法定义为抽象方法。

    抽象方法是指没有具体实现的方法,即没有方法体。在 TypeScript 中,抽象方法必须存在于抽象类中。抽象类使用 abstract 关键字声明,包含抽象方法的类就称为抽象类。

    // 抽象类 Shape
    abstract class Shape {
        abstract getArea(): number; // 抽象方法,没有具体实现
    class Rectangle extends Shape {
        private width: number;
        private height: number;
        constructor(width: number, height: number) {
            super();
            this.width = width;
            this.height = height;
        getArea(): number { // 实现抽象类中的 getArea 抽象方法
            return this.width * this.height;
    class Circle extends Shape {
        private r: number;
        constructor(r: number) {
            super();
            this.r = r;
        getArea(): number { // 实现抽象类中的 getArea 抽象方法
            return 3.14 * this.r * this.r;
    function makeArea(shape: Shape) {
        return shape.getArea(); // 多态的应用
    const rectangle = new Rectangle(10, 20);
    const circle = new Circle(10);
    console.log(makeArea(rectangle));
    console.log(makeArea(circle));
    

    抽象类 Shape 具有以下特点:

  • Shape 抽象类不能被实例化,也就是说,无法通过new 关键字创建对象。
  • Shape中的 getArea 抽象方法必须由子类 Rectangle 和 Circle 实现,否则该类必须也是一个抽象类。
  • 类作为数据类型使用

    类不仅可以用于创建对象,还可以用作一种数据类型。

    示例代码:

    class Person {
        name: string = '张三';
        eating() {
    const p = new Person(); // 用类创建对象
    const p1: Person = { // 类作为一种数据类型使用
        name: '李四',
        eating() {
    function printPerson(p: Person) { // 类作为数据类型使用
        console.log(p.name);
    printPerson(new Person()); // 张三
    printPerson(p1); // 李四
    printPerson({name: 'tom',eating(){}}); // tom
    

    从上面代码可以看出,Person 类不仅可以用于创建 Person 对象,还可以用作数据类型。

    TypeScript 接口的使用

    面向对象编程具有封装、继承、多态三大特性,而接口是封装实现的最重要的概念之一

  • 接口是在应用程序中定义一种约定的结构,在定义时要遵循类的语法。
  • 从接口派生的类必须遵循其接口提供的结构,并且TypeScript 编译器不会将接口转换为JavaScript
  • 接口是用关键字 interface 定义的,并将 class 改成 interface。格式为 interface接口名
  • 接口中的功能没有具体实现,接口不能实例化,也不能使用new 关键字创建对象。
  • 接口的声明

    下面以接口的方式为对象声明类型。

    示例代码:

    // 通过接口(interface) 声明对象类型
    interface InfoType {
        readonly name: string // 只读属性
        age?: number // 可选属性
        height: number
    // 指定对象的类型
    const info: InfoType = {
        name:'zhangsan',
        age: 20,
        height: 170
    console.log(info.name);
    // info.name = 'lisi'; // 只读属性不能修改
    info.age = 22; // 可以修改
    

    如上面代码所示,我们使用关键字 interface 定义一个 InfoType 接口。另外,我们也可以用接口声明函数类型等。

    使用 interface 定义对象类型时,要求对象的属性名、方法以及类型都是确定的。但有时会遇到一些特殊情况,例如所有的 key 或者 value 的类型都是相同的,这时可以使用索引类型。

    如下代码:

    const languageType = {
        0: 'css',
        1: 'html',
        2: 'javascript',
        3: 'vue'
    const languageYear = {
        "javascript": 1996,
        "c": 1972,
        "java": 1995,
        "python": 1989
    

    上面的对象用一个共同的特点:key 的类型和 value 的类型是相同的。在这种情况下,需要使用索引签名来约束对象中的属性和值的结构及类型。

    示例代码如下:

    interface IndexLanguage  {
        [index: number]: string
    interface IlanguageYear {
        [key: string]: number
    

    从上述代码中可以看到,我们使用 interface 定义了两个索引类型:IndexLanguage 和 ILanguageYear。在 IndexLanguage 接口中,通过计算属性的方式约定对象的属性的 key 是 number类型 ,value 是 string 类型,计算属性中的 index 是支持任意命名的。另 一个索引类型同理。

    具体的属性可以和索引签名混合在一起使用,但是属性的类型必须是索引值的子集,示例代码:

    interface LanguageY {
        [name: string]: number,
        java: number
    const language: LanguageY = {
        java: 1995,
        javascript: 1996,
        python: 1989
    

    场景示例:

    如下示例,创建一个函数 getValues,该函数旨在从给定对象中提取指定属性的值并返回这些值组成的数组。此函数接两个参数:

  • 第一个参数为 obj,即待操作的对象,它包含若干键值对。
  • 第二个参数为 keys,这是一个字符串数组,表示我们希望从 obj 中获取哪些属性的值。
  • // 场景
    const obj = {
        a: 1,
        b: 2,
    function getValues(obj: any, keys: string[]) {
        return keys.map(key => obj[key])
    console.log(getValues(obj, ['a', 'b'])); // [ 1, 2 ]
    console.log(getValues(obj, ['a', 'b', 'd'])); // [ 1, 2, undefined ]
    

    尽管 obj 中没有键 'd',函数仍会尝试访问该属性,但不会抛出错误,而是将 undefined作为该属性的默认返回值加入结果数组,因此输出结果为 [1, 2, undefined]。如果我们希望在调用不存在的属性的时候,希望ts报错提示。

    在改造之前,我们先来看下下面的示例,keyof T的用法:

    // keyof T 用法
    interface Obj {
        a: number;
        b: string;
    let k: keyof Obj;
    

    上面代码中,k变量的类型被指定为keyof Obj,keyof是一个TypeScript的类型操作符,它取出一个类型的所有属性名并形成一个联合类型。在这个例子中,keyof Obj的结果是"a" | "b",意味着k变量可以被赋值为字符串"a"或"b",这两个字符串代表了Obj接口中定义的属性名。

    T[K] 的用法:

    // T[K]
    let value: Obj['a']
    

    这里 Obj['a'] 是一个索引类型查询表达式,它用来从接口 Obj 中获取键为 'a' 的属性所对应的类型。根据 Obj 接口的定义,键 'a' 对应的类型是 number。因此,Obj['a'] 的结果就是 number 类型。

    所以,let value: Obj['a']; 这行代码声明了一个名为 value 的变量,其类型被限定为 number,这意味着 value 变量只能被赋予数值类型的值。这是一种利用接口属性类型进行类型安全引用的方式,有助于提高代码的可读性和健壮性。

    改造上面的代码:

    function getValues<T,K extends keyof T>(obj: T, keys: K[]): T[K][] {
        return keys.map(key => obj[key]);
    

    泛型定义:

  • T 是一个泛型类型参数,表示传入函数的第一个参数 obj 的类型。它可以是任何对象类型。
  • K extends keyof T 是另一个泛型类型参数,其中 K 必须是 T的键(即对象的属性名)的类型。这意味着 K 可以是 T 类型上任意属性名的类型。
  • 函数签名:

  • obj: T:表示函数接受的第一个参数 obj 是类型为 T 的对象。
  • keys: K[]:表示函数接受的第二个参数 keys 是一个数组,数组中的元素类型为 K,即它们必须是 T 类型对象的键的类型。
  • 返回类型:

  • T[K][]:表示返回值是一个数组,数组中的元素类型为 T 类型对象中键为 K 的属性值的类型。简单来说,就是根据提供的属性名列表,返回这些属性值组成的数组。
  • 这种泛型函数设计让 getValues 非常灵活,可以应用于任何具有相应属性的对象,同时保证了类型安全,避免了因错误的属性名导致的运行时错误。

    Interface不仅可以定义对象中的属性和方法,实际上 interface 还可以用于定义函数类型。

    示例代码:

    // 方式一:使用 type 定义函数类型
    // type addType = (n1: number, n2: number) => number;
    // 使用 interface 定义函数类型
    interface calcFn {
      (n1: number, n2: number): number;
    // 指定 add 函数的类型
    const add: calcFn = (n1, n2) => {
      return n1 + n2;
    // 指定参数 calcFn 函数的类型
    function calc(n1: number, n2: number, calcFn: calcFn) {
        return calcFn(n1, n2);
    console.log(add(1,2));
    console.log(calc(1,2,add))
    

    可以看到,定义函数的类型有两种方案: 一种是使用类型别名,另一种是使用接口。如果只是定义一个普通的函数,推荐使用类型别名,这样可以提高代码的可读性。但是,如果需要更强的扩展性,就需要函数具有使用接口来定义函数的类型。

    示例代码:

    // 用 interface 定义函数的类型,该函数还有 get 和 post 属性
    interface FakeAxiosType {
        (config: any): Promise<any>,
        get: (url: string) => string,
        post: (url: string) => string
    const FakeAxios: FakeAxiosType = function(config: any) {
        return Promise.resolve(config);
    FakeAxios.get = function(url: string): string {
        return 'getcontent'
    FakeAxios.post = function(url: string): string {
        return 'postcontent'
    console.log(FakeAxios({url: 'https://www.baidu.com'})); // Promise { { url: 'https://www.baidu.com' } }
    console.log(FakeAxios.get('https://www.baidu.com')); // getcontent
    console.log(FakeAxios.post('https://www.baidu.com')); // postcontent
    

    从上面代码可以看出,定义的接口对象 FakeAxiosType 有三个成员:一个函数、一个 get 方法和一个 post 方法。

    场景示例:

    interface Square {
        shape: "square";
        size: number;
    interface Rectangle {
        shape: "rectangle";
        width: number;
        height: number;
    interface Circle {
        shape: "circle";
        radius: number;
    type Shape = Square | Rectangle | Circle
    function area(s: Shape) {
        switch (s.shape) {
            case "square":
                return s.size * s.size;
            case "rectangle":
                return s.height * s.width;
            case 'circle':
                return Math.PI * s.radius ** 2
            default:
                return ((e: never) => {throw new Error(e)})(s)
    console.log(area({shape: 'circle', radius: 1}))
    

    上面示例代码中 area函数的 default 分支使用了一个技巧来处理不符合任何已知 shape的情况。这里,它接受一个 never 类型的参数 e(因为理论上这个分支不应该被执行),然后抛出一个错误。这实际上是一个类型安全的做法,因为 TypeScript 会在编译时检查并确保没有遗漏的 shape 值。

    接口的继承

    在 TypeScript 中,接口和类一样可以实现继承。接口的继承同样使用 extends 关键字,接口支持多继承,而类不支持多继承

    示例代码:

    interface ISwim {
        swimming: () => void
    interface IRun {
        running: () => void
    // 接口的多继承
    interface IAction extends ISwim, IRun {}
    const action: IAction = {
        swimming: () => {
            console.log('swimming')
        running: () => {
            console.log('running')
    console.log(action.running())
    

    可以看到,IAction接口继承了 ISwim 和 IRun 两个接口。将 action 对象指定类型为IAction, 这意味着该对象必须实现 IAction 接口中定义的所有属性和方法。

    前面我们已经了解过联合类型,它表示满足多个类型中的任意一个,示例代码如下:

    type Alignment ='left'|'right'|'center';
    

    其实,还有一种类型合并方式——交叉类型 (Intersection Type), 它表示需要同时满足多个类型的条件,使用符号&连接。

    例如,下面的交叉类型 MyType 表示需要同时满足 number 和 string 类型。然而,实际上这
    种类型是不存在的,因此 MyType 实际上是一个 never 类型。

    type MyType = number & string;
    

    在实际开发中,交叉类型通常用于合并对象类型。示例代码如下:

    interface ISwim {
        swimming: () => void
    interface IRun {
        running: () => void
    type MyType1 = ISwim | IRun;
    type MyType2 = ISwim & IRun;
    // 联合类型
    const obj1: MyType1 = {
        swimming() {
            console.log('swimming')
    // 交叉类型
    const obj2: MyType2 = {
        swimming() {
            console.log('swimming')
        running() {
            console.log('running')
    

    从上面可以看出,交叉类型 MyType2 是 ISwim 和 IRun 类型的合并,而不是交集。

    interface DogInterface {
        run(): void
    interface CatInterface {
        jump(): void
    let pet: DogInterface & CatInterface = {
        run() {},
        jump() {}
    

    接口的实现

    接口除了可以被继承,还可以被类实现。如果一个类实现了该接口,那么在需要传入该接口的地方,都可以传入该类对应的对象,这就是面向接口编程的思想。

    示例代码:

    interface ISwim {
        swimming: () => void;
    interface IRun {
        running: () => void;
    class Animal {}
    // 继承(extends): 只能实现单继承
    // 实现接口(implements): 类可以实现多个接口
    class Dog extends Animal implements ISwim, IRun {
        swimming() {
            console.log('dog swimming')
        running() {
            console.log('dog running')
    class Person implements ISwim {
        swimming() {
            console.log('person swimming')
    // 编写一些公共的API 。下面是面向接口编程,即 swimAction 函数接收的是ISwim接口
    function swimAction(swim: ISwim) {
        swim.swimming();
    // 只要实现了 ISwim 接口的类对应的对象,都可以传给 swimAction 函数
    console.log(swimAction(new Dog())); // dog running
    console.log(swimAction(new Person())); // person swimming
    

    从上面代码可以看出,Dog 类继承了 Animal 类,并且实现了 ISwim 和 IRun 接口,Person类也实现了 ISwim 接口。接着,我们编写一个 swimAction函数,该函数需要接收一个 ISwim接口,这意味着它可以接收所有实现了 ISwim 接口的类对应的对象,也就是常说的面向接口编程。

    interface DogInterface {
        run(): void
    interface CatInterface {
        jump(): void
    class Dog implements DogInterface {
        run() {
            console.log('dog run');
        eat() {
            console.log('dog eat');
    class Cat implements CatInterface {
        jump() {
            console.log('cat jump');
        eat() {
            console.log('cat eat');
    enum Master { Dog, Cat }
    function getPet(master: Master) {
        let pet = master === Master.Dog ? new Dog() : new Cat();
        // 这儿只能调用交集的方法
        pet.eat();
        return pet
    const dog = getPet(Master.Dog);
    const cat = getPet(Master.Cat);
    

    上述示例代码解释如下:

    接口定义:

  • DogInterface 接口定义了一个 run 方法,没有参数,返回类型为 void。
  • CatInterface 接口定义了一个 jump 方法,同样没有参数,返回类型为 void。
  • Dog 类实现了 DogInterface 接口,因此它必须提供 run 方法的实现。它还额外定义了一个 eat 方法。
  • Cat 类实现了 CatInterface 接口,因此它必须提供 jump 方法的实现。它也额外定义了一个 eat 方法。
  • 枚举定义:

  • Master 枚举定义了两个成员:Dog 和 Cat。这些成员默认会被赋予从 0 开始的整数值(Dog 为 0,Cat 为 1)。
  • 函数定义:

  • getPet 函数接受一个 Master 枚举类型的参数 master。
  • 使用条件(三元)运算符来判断 master 的值,并基于该值创建并返回相应的 Dog 或 Cat 类的实例。
  • 在函数内部,通过 pet 变量引用了创建的 Dog 或 Cat 实例。由于 Dog 和 Cat 类都定义了 eat 方法,所以可以在这里调用 pet.eat()。但请注意,由于 pet 的类型在编译时是未知的(它是一个 Dog 或 Cat 的联合类型),你不能直接调用 pet.run() 或 pet.jump(),除非你使用类型断言或类型守卫来明确它的类型。
  • interface 和 type 的区别

    在实际开发中,interface 和 type 都可以用于定义对象类型,主要有以下选择方式:

  • 在定义非对象类型时,通常推荐使用 type。比如 Direction、Alignment以及一些 Function。
  • 定义对象类型时,interface 和 type 有如下区别:
  • interface:可以重复地对某个接口定义属性和方法。
  • type: 定义的是别名,别名不能重复。
  • 示例代码:

    interface Info {
        name: string
    interface Info {
        age: number
    // Info 类型是上面两个 Info 接口的合并
    const info: Info = {
        name: '张三',
        age: 18
    

    从上面代码可以看出,interface 可以重复对某个接口进行定义。比如,当我们想为 window 扩展额从的属性时,可以重复定义 window 的类型。

    下面来看看不能重复定义类型别名的情况,示例代码:

    type Pinfo = { // 报错:Duplicate identifier 'Pinfo'.
        name: string
    type Pinfo = { // 报错:标识符 'Pinfo' 重复。
        age: number
    

    从上面代码可以看出,类型别名 Pinfo 是不能重复定义的。

    字面量赋值

    前面已经介绍过,接口可以为对象声明类型。示例代码:

    interface Iperson {
        name: string;
    const info: Iperson = {
        name: '张三',
        age: 18 // 报错:Object literal may only specify known properties, and 'age' does not exist in type 'Iperson'.
    

    可以看到,这里使用 interface 定义一个 IPerson 对象的类型,接着将该类型指定给 info对象。由于info 对象中多出一个 age 属性,而该属性没有在 IPerson中声明过,因此会提示报错。

    针对这个报错有多种解决方案,比如:增加 IPerson 中 的 age 属性、使用索引类型或交叉
    类型等。这里介绍另一种方案:使用字面量赋值。代码如下:

    interface Iperson {
        name: string;
    const info = {
        name: '张三',
        age: 18 
    const p: Iperson = info;
    

    这里将 info 字面量对象赋值给类型为 IPerson 的 p 变量。在字面量赋值的过程中,TypeScript在类型检查时会保留IPerson类型,同时擦除其他类型。如果将字面量对象直接赋值给函数的参数,也是同样的道理,示例代码如下:

    interface Iperson {
        name: string;
    const info = {
        name: '张三',
        age: 18 
    function printInfo(person: Iperson) {
        console.log(person);
    // 将字面量对象直接传给函数的参数
    printInfo({ 
        name: '张三', 
        age: 18 // 报错
    // 对字面量对象的引用,传递给函数参数
    printInfo(info); // 正确
    

    使用场景示例

    例如,我们需要给后端的返回的接口数据进行处理,那么我们一般进行如下接口定义:

    interface List {
        readonly id: number;
        name: string;
        age?: number;
        [x: string]: any;
    interface Result {
        data: List[]
    function render(result: Result) {
        result.data.forEach((value) => {
            // 拿到数据做业务操作
            console.log(value.id, value.name)
            if (value.age) {
                console.log(value.age)
    // 模拟后端返回的数据
    let result = {
        data: [
            {id: 1, name: 'A', sex: 'male'},
            {id: 2, name: 'B', age: 10}
    render(result);
    

    如下示例,我们要定义一个Lib类库,那么我们可以使用接口来定义:

    interface Lib {
        (): void;
        version: string;
        doSomething(): void;
    function getLib() {
        let lib = (() => {}) as Lib;
        lib.version = '1.0.0';
        lib.doSomething = () => {};
        return lib;
    let lib1 = getLib()
    lib1();
    let lib2 = getLib();
    lib2.doSomething();
    

    TypeScript 枚举类型

    看一下面的这个代码:

    function initRole(role: number) {
        if(role === 1 || role === 2) {
            // do something
        } else if(role === 3 || role === 4) {
            // do something
        } else if (role === 5 ) {
            // do something
        } else {
            // do something
    

    存在的问题:

  • 可读性差:很难记住数字的含义。
  • 可维护性差:硬编码,牵一发动全身。
  • 枚举:一组有名字的常量集合。

    枚举不是 JavaScript 中的类型,而是 TypeScript 的少数功能之一。

  • 枚举是指将一组可能出现的值逐个列举出来并定义在一个类型中,这个类型就是枚举类型。
  • 开发者可以使用枚举定义一组命名常量,这些常量可以是数字或字符串类型。
  • 枚举类型使用 enum 关键字来定义。
  • 示例代码:

    // 定义 Direction 枚举
    enum Direction {
        LEFT,
        RIGHT
    // 指定 direction 参数为 Direction 枚举类型
    function move(direction: Direction) {
        switch(direction) {
            case Direction.LEFT:
                console.log('向左移动');
                break;
            case Direction.RIGHT:
                console.log('向右移动');
                break;
            default:
                const foo: never = direction; // 确保枚举的每个成员都被处理过
                break;
    // 使用枚举,调用 move 函数时传入对应的枚举项
    move(Direction.LEFT);
    move(Direction.RIGHT);
    

    这里使用 enum 关键字定义了Direction枚举类型,该枚举定义了LEFT 和RIGHT两个成员。接着,将 move 函数的参数指定为 Direction 枚举类型。当调用 move 函数时,传入对应枚举的成员,这就是枚举的定义和简单的使用过程。

    枚举类型的成员默认是有值的,示例代码如下:

    // 定义 Direction 枚举
    enum Direction {
        LEFT, // 默认值0
        RIGHT // 默认值1
    console.log(Direction.LEFT); // 0
    console.log(Direction.RIGHT); // 1
    

    从上面代码可以看出,如果没有指定枚举成员的值,则默认从零开始自增长。

    当然我们也可以给枚举成员的值重新赋值其他的值,示例代码:

    // 定义 Direction 枚举
    enum Direction {
        LEFT = 100, 
        RIGHT // 自增长
    console.log(Direction.LEFT); // 100
    console.log(Direction.RIGHT); // 101
    

    我们还可以将枚举成员的值赋值为字符串,示例代码如下:

    // 定义 Direction 枚举
    enum Direction {
        LEFT, 
        RIGHT = 'RIGHT'
    console.log(Direction.LEFT); // 0
    console.log(Direction.RIGHT); // RIGHT
    

    ① 数字枚举:

    enum Role {
        Reporter = 1,
        Developer,
        Maintainer,
        Owner,
        Guest
    

    最终编译的JS效果(解析原理):

    "use strict";
    var Role;
    (function (Role) {
        Role[Role["Reporter"] = 1] = "Reporter";
        Role[Role["Developer"] = 2] = "Developer";
        Role[Role["Maintainer"] = 3] = "Maintainer";
        Role[Role["Owner"] = 4] = "Owner";
        Role[Role["Guest"] = 5] = "Guest";
    })(Role || (Role = {}));
    

    那么执行下面的代码,可以看到如下结果:

    console.log(Role.Reporter,Role['1']); // 1 Reporter
    console.log(Role);
    

    其中打印 Role 的结果如下:

    '1': 'Reporter', '2': 'Developer', '3': 'Maintainer', '4': 'Owner', '5': 'Guest', Reporter: 1, Developer: 2, Maintainer: 3, Owner: 4, Guest: 5

    ② 字符串枚举:

    enum Message {
        Success = '恭喜你,成功了',
        Fail = '抱歉,失败了'
    console.log(Message.Success); // 恭喜你,成功了
    console.log(Message.Fail); // 抱歉,失败了
    

    解析成JS:

    "use strict";
    var Message;
    (function (Message) {
        Message["Success"] = "\u606D\u559C\u4F60\uFF0C\u6210\u529F\u4E86";
        Message["Fail"] = "\u62B1\u6B49\uFF0C\u5931\u8D25\u4E86";
    })(Message || (Message = {}));
    

    ③ 异构枚举(数字枚举和字符串枚举混合使用,不建议使用):

    enum Answer {
        Y = 'Yes'
    

    解析成JS:

    "use strict";
    var Answer;
    (function (Answer) {
        Answer[Answer["N"] = 0] = "N";
        Answer["Y"] = "Yes";
    })(Answer || (Answer = {}));
    

    ④ 常量枚举:

    const enum Month {
        Apr = Month.Mar + 1,
        // May = () => 5 // 报错:函数本质上是动态的,其值在运行时计算得出, 不能被编译阶段计算
    let month = [Month.Jan, Month.Feb, Month.Mar, Month.Apr];
    console.log(month); // [0, 1, 2, 3]
    

    编译成JS:

    "use strict";
    let month = [0 /* Month.Jan */, 1 /* Month.Feb */, 2 /* Month.Mar */, 3 /* Month.Apr */];
    console.log(month); // [0, 1, 2, 3]
    

    ⑤ 枚举成员:

    enum Char {
        // 常量枚举,三种情况,会在编译的时候计算出结果,然后以常量的形式出现在运行时环境
        a, // 没有初始值
        b = Char.a, // 对已有枚举成员的引用
        c = 1 + 3, // 常量表达式
        // 需要被计算的枚举成员,非常量的表达式,这些枚举值不会在编译阶段进行计算,而是会保留到运行阶段
        d = Math.random(),
        e = '123'.length
    

    解析成JS:

    "use strict";
    var Char;
    (function (Char) {
        // 常量枚举,三种情况,会在编译的时候计算出结果,然后以常量的形式出现在运行时环境
        Char[Char["a"] = 0] = "a";
        Char[Char["b"] = 0] = "b";
        Char[Char["c"] = 4] = "c";
        // 需要被计算的枚举成员,非常量的表达式,这些枚举值不会在编译阶段进行计算,而是会保留到运行阶段, 运行阶段才会被计算。
        Char[Char["d"] = Math.random()] = "d";
        Char[Char["e"] = '123'.length] = "e";
    })(Char || (Char = {}));
    

    如上面示例,当在被需要计算的枚举成员后面添加新的枚举成员,那么这个成员需要赋值,否则会报错,如下示例:

    // 枚举成员
    enum Char {
        // 常量枚举,三种情况,会在编译的时候计算出结果,然后以常量的形式出现在运行时环境
        a, // 没有初始值
        b = Char.a, // 对已有枚举成员的引用
        c = 1 + 3, // 常量表达式
        // 需要被计算的枚举成员,非常量的表达式,这些枚举值不会在编译阶段进行计算,而是会保留到运行阶段
        d = Math.random(),
        e = '123'.length,
        f  // 报错
    

    那需要调整为如下代码,给该枚举成员赋值才不会报错:

    // 枚举成员
    enum Char {
        // 常量枚举,三种情况,会在编译的时候计算出结果,然后以常量的形式出现在运行时环境
        a, // 没有初始值
        b = Char.a, // 对已有枚举成员的引用
        c = 1 + 3, // 常量表达式
        // 需要被计算的枚举成员,非常量的表达式,这些枚举值不会在编译阶段进行计算,而是会保留到运行阶段
        d = Math.random(),
        e = '123'.length,
        f = 42 // ok
    

    TypeScript泛型的使用

    不预先确定的数据类型,具体的类型在使用的时候才能确定。

    将参数的类型也进行参数化,这就是通常所说的类型参数化,也称为泛型。

    为了更好地理解类型参数化,下面来看一个需求:封装一个函数,传入一个参数,并且返回这个参数。示例代码:

    function foo(arg: number): number {
        return arg;
    

    可以看到,foo函数的参数和返回值类型应该一致,都为 number 类型。虽然该代码实现了功能,但是不适用于其他类型,如string、boolean等。有人可能会建议将 number 类型改为 any 类型,但这样会丢失类型信息。例如传入的是一个number,我们希望返回的不是 any 类型,而是 number 类型。因此,在函数中需要捕获参数的类型为 number ,并将其用作返回值的类型。

    这时,我们需要使用一种特殊的变量——类型变量(Type Variable),用于声明类型,代码如下:

    function foo<Type>(arg: Type): Type {
        return arg;
    

    可以看到,使用 <>语法 在 foo 函数中定义了一个类型变量 Type, 接着该类型变量用于声明变量的类型函数返回值的类型,这就是泛型的定义。

    在foo函数中定义好类型变量 Type 之后,接下来在调用 foo 函数时,也可以使用 <> 语法为类型变量Type 传递具体的类型,这就是泛型的使用。

    示例代码:

    // 调用方式一:向类型变量 Type 传递具体的类型
    foo<number>(123);
    foo<{ name: string }>({ name: '张三' });
    // 调用方式二: TypeScript 会自动推导出 Type 具体的类型
    foo(123);
    foo({ name: '张三' });
    foo('zhangsan');
    

    需要注意的是,foo 函数上可以定义多个类型变量,示例代码如下:

    function info<T,E>(arg1: T, arg2: E) {
        console.log(arg1, arg2);
    info<number, string>(123, '张三');
    info<{ name: string }, { age: number }>({ name: '张三' }, { age: 18 });
    info(123, '张三');
    

    在 foo 函数中定义了两个类型变量 T 和 E, 这两个变量的名称可以任意命名。

    在开发中,通常会使用以下名称来定义类型变量:

  • T:Type的缩写,表示类型。
  • K、V:key 和 value 的缩写,表示键值对。
  • E: Element的缩写,表示元素。
  • 0: Object 的缩写,表示对象。
  • 泛型的应用非常广泛,不仅可以在函数中使用,还可以在定义接口时使用。

    示例代码:

    // 定义接口,在接口中定义T1和T2两个类型变量,并且都有默认值
    interface IPerson<I1 = string, T2 = number> {
        name: I1,
        age: T2
    // 将p1和p2指定为 IPerson 类型
    const p1: IPerson = {
        name: 'zhangsan',
        age: 18
    const p2: IPerson<string, number> = {
        name: 'zhangsan',
        age: 18
    

    可以看到,在 IPerson 接口中定义了 T1 和 T2 两个类型变量,T1的默认类型为string, T2的默认类型为 number。也就是说,该接口中的 name 属性的默认类型为 string,age 属性的默认类型为 number。

    然后,将 p1 和 p2 都指定为 IPerson 类型,其中在指定 p2 时,又为 T1 和 T2 类型变量传递了具体的类型,这就是泛型在接口中的用法。

    在定义类时也可以使用泛型,如下示例:

    // 在 Point 类上定义 T 类型变量
    class Point<T> {
        x: T;
        y: T;
        z: T;
        constructor(x: T, y: T,z: T) {
            this.x = x;
            this.y = y;
            this.z = z;
    // ts 会自动推导 T 类型变量的具体类型
    const p1 = new Point(1, 2, 3);
    // 向 Point 类的T类型变量传递具体的number类型
    const p2 = new Point<number>(1, 2, 3);
    const p3: Point<number> = new Point(1, 2, 3);
    

    可以看到,在Point 类中定义了一个T 类型变量:

  • 在创建 pl 对象时, TypeScript 会自动推导出 T 类型变量的具体类型。
  • 在创建 p2 对象时,向 T 类型变量传递了具体的 number 类型。
  • 在声明 p3 对象类型时,也向 T 类型变量传递了具体的 number 类型。
  • 有时我们希望传入的类型有某些共性,但是这些共性可能不在同一种类型中。例如,string 和 array 都有 length 属性,或者某些对象也会有 length 属性。这意味着只要是拥有 length 属性的类型,都可以作为参数类型。这时,需要使用泛型约束来定义类型。

    示例如下:

    // 接口定义对象类型
    interface Ilength {
        length: number
    // 在 getLength 函数中定义T类型变量,并添加类型的约束
    // T类型必须包含ILength接口中定义的 length 属性
    function getLength<T extends Ilength>(arg: T) {
        return arg.length;
    // 泛型约束的使用
    getLength('abc'); // TypeScript 会自动推导出 string 类型(string 有 length 属性)
    getLength<string[]>(['a', 'b']); // 向 T 类型变量传递 string[] 类型(该类型有 length 属性)
    getLength<{length: number}>({length: 10}); // 向 T 类型变量传递{length:number}对象类型(有 length 属性)
    // getLength<number>(100); // 报错,因为 number 没有 length 属性
    

    可以看到,在 getLength 函数中定义了 T 类型变量,并且通过 extends 关键字为该类型添加了约束,约束 T 类型必须包含 Ilength 接口中定义的length 属性 。在调用 getLength 函数时,传入的参数类型必须包含 length 属性,这就是泛型约束的用法。

    泛型的好处:

  • 增强程序的可扩展性:函数或类可以很轻松的支持多种数据类型。
  • 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明。
  • 灵活的控制类型之间的约束。
  • TypeScript类型检查机制

    TypeScript能够在特定的区块中保证变量属于某种确定的类型。可以在此区块中可以放心的引用该类型的属性,或者调用该类型的方法。

    如下示例:

    enum Type { Strong, Weak}
    class Java {
        helloJava() {
            console.log('hello java');
    class JavaScript {
        helloJavaScript() {
            console.log('hello javascript');
    function getLanguage(type: Type) {
        const lang = type === Type.Strong ? new Java() : new JavaScript();
        if(lang.helloJava) { // 报错:类型“Java | JavaScript”上不存在属性“helloJava”。
            lang.helloJava();
        } else {
            lang.helloJavaScript();
        return lang;
    

    一般上面这样实现是会报错的,调整下代码,使用类型断言:

    function getLanguage(type: Type) {
        const lang = type === Type.Strong ? new Java() : new JavaScript();
        if((lang as Java).helloJava) {
            (lang as Java).helloJava();
        } else {
            (lang as JavaScript).helloJavaScript();
        return lang;
    

    但是这样实现后,发现在每次使用对应的属性跟方法的话都需要使用类型断言才行,很麻烦,接下来,我们使用如下几种方式的类型保护。

  • 1.instanceof 类型保护
  • function getLanguage(type: Type) {
        const lang = type === Type.Strong ? new Java() : new JavaScript();
        // instanceof 类型保护
        if(lang instanceof Java) {
            lang.helloJava();
        } else {
            lang.helloJavaScript();
        return lang;
    
  • 2.自定义函数类型保护
  • function isJava(lang: Java | JavaScript): lang is Java {
        return (lang as Java).helloJava !== undefined;
    function getLanguage(type: Type) {
        const lang = type === Type.Strong ? new Java() : new JavaScript();
        // 自定义函数类型保护
        if(isJava(lang)) {
            lang.helloJava();
        } else {
            lang.helloJavaScript();
        return lang;
    

    上面示例中定义一个函数 isJava,该函数执行某种检查并返回一个类型谓词,该谓词是一个返回 x is T 的表达式,其中 T 是一个类型。

    当然类型保护还有typeof、in等类型保护方式。

    typeof 类型保护示例:

    function isString(x: any): x is string {  
        return typeof x === 'string';  
    function isNumber(x: any): x is number {  
        return typeof x === 'number';  
    // 使用示例  
    let value: any = 'Hello, TypeScript!';  
    if (isString(value)) {  
        console.log(value.toUpperCase()); // 没有问题,因为 TypeScript 知道 value 是 string  
    } else {  
        console.log(value.toFixed(2)); // 这里不会执行,因为 value 不是 number  
    

    in 类型保护示例:

    // in 类型保护示例
    interface Square {  
        shape: "square";  
        size: number;  
    interface Circle {  
        shape: "circle";  
        radius: number;  
    type Shape = Square | Circle;  
    // 类型守卫函数,模拟基于 'radius' 属性的类型保护  
    function isCircle(shape: Shape): shape is Circle {  
        return "radius" in shape;  
    function getArea(shape: Shape) {
        if (isCircle(shape)) {  
            return Math.PI * shape.radius * shape.radius;  
        } else  {  
            return shape.size * shape.size;
    // 使用示例  
    const square: Square = { shape: "square", size: 5 };  
    const circle: Circle = { shape: "circle", radius: 2.5 };  
    console.log(getArea(square)); // 输出 25  
    console.log(getArea(circle)); // 输出 19.625(π取3.14近似值)
    

    在这段代码中,我们定义了两个接口 Square 和 Circle,它们分别表示正方形和圆形,并包含各自的属性。接着,我们定义了一个类型别名 Shape,它可以是 Square 或 Circle 类型的联合。

    然后,我们定义了一个类型守卫函数 isCircle,它接收一个 Shape 类型的参数 shape,并检查该对象是否有一个 radius 属性。如果存在 radius 属性,则返回 true,表明该对象是圆形(Circle 类型),这就是类型守卫的工作原理。但是,这里有一个潜在的问题:虽然 isCircle 函数在逻辑上可能按预期工作,但它并不是一个完全安全的类型守卫,因为理论上任何对象都可以有一个 radius 属性,而不仅仅是 Circle 类型的对象。

    TypeScript高级类型

    在 TypeScript 中,映射类型(Mapped Types)是一种基于一个已知的类型来创建新类型的方式。它基于旧类型中的每个属性来生成的属性,从而允许你对现有类型进行转换或修改。

    映射类型的一般语法如下:

    type MappedType<T> = {  
        [P in keyof T]: /* some transformation of T[P] */;  
    

    这里 keyof T 获取了类型 T 的所有键(key),而 [P in keyof T]: ... 则是对每个键 P 及其对应的值类型 T[P] 进行转换的表达式。

    如下示例:

    interface Obj {
        name: string;
        age: number;
        graduate: boolean;
    // 只读属性,所有属性都变为只读 
    type ReadonlyObj = Readonly<Obj>;
    // 可选属性,所有属性都变为可选 
    type PartialObj = Partial<Obj>;
    // 挑选属性,只包含'name'和'age'属性
    type PickObj = Pick<Obj, 'name' | 'age'>;
    // 构造一个具有类型Obj的对象集合,键为'x'和'y'
    type RecordObj = Record<'x' | 'y', Obj>;
    

    上面代码类型别名ReadonlyObj、PartialObj、PickObj、RecordObj对应的解析的结果:

    type ReadonlyObj = {
        readonly name: string;
        readonly age: number;
        readonly graduate: boolean;
    type PartialObj = {
        name?: string | undefined;
        age?: number | undefined;
        graduate?: boolean | undefined;
    type PickObj = {
        name: string;
        age: number;
    type RecordObj = {
        x: Obj;
        y: Obj;
    

    内置工具类型Readonly、Partial、Pick、Record对应的源码实现:

    // 只读属性
    type Readonly<T> = {
        readonly [P in keyof T]: T[P];
    // 可选属性
    type Partial<T> = {
        [P in keyof T]?: T[P];
    // 挑选属性
    type Pick<T, K extends keyof T> = {
        [P in K]: T[P];
    // 构造一个具有类型T的一组属性K的类型
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    
    T extends U ? X : Y
    

    在 TypeScript 中,T extends U ? X : Y 是一个条件类型(conditional type),它用于根据某个类型 T 是否是另一个类型 U 的子类型(或相同类型)来返回两种类型之一:X 或 Y。

    这种条件类型在多个场景中都非常有用:

  • 1.类型保护
    当你想根据某个类型来决定如何操作某个值时,条件类型可以帮助你创建类型保护。虽然条件类型本身并不直接提供类型保护,但结合泛型、函数和类型断言,它们可以构建出强大的类型保护系统。
  • 如下示例:

    // 假设我们有一个联合类型,它可以是字符串或数字  
    type StringOrNumber = string | number;  
    // 定义一个泛型函数,该函数接受一个 StringOrNumber 类型的参数  
    function isString<T extends StringOrNumber>(value: T): boolean {  
      // 使用 typeof 操作符进行类型检查  
      return typeof value === 'string';  
    // 示例使用  
    function handleValue(value: StringOrNumber) {  
      if (isString(value)) {  
        const stringValue = value as string;
        console.log(`字符串长度: ${stringValue.length}`);  
      } else {  
        console.log(`数字值: ${value}`);  
    

    我们使用类型保护来实现:

    // 假设我们有一个联合类型,它可以是字符串或数字  
    type StringOrNumber = string | number;  
    // 返回一个类型保护,检查该参数是否为字符串  
    function isString(value: StringOrNumber): value is string {  
      // 使用 typeof 操作符进行类型检查  
      return typeof value === 'string';  
    // 示例使用  
    function handleValue<T extends StringOrNumber>(value: T) {  
      if (isString(value)) {  
        // 在这里,TypeScript 知道 value 一定是 string 类型  
        console.log(`字符串长度: ${value.length}`);  
      } else {  
        // 在这里,TypeScript 知道 value 一定是 number 类型  
        console.log(`数字值: ${value}`);  
    // 测试函数  
    handleValue('hello'); // 输出 "字符串长度: 5"  
    handleValue(42); // 输出 "数字值: 42"
    
  • 2.类型映射
    你可以使用条件类型来映射一个类型的所有属性到一个新的类型。例如,你可能想要将某个对象类型的所有属性从 string 转换为 number(当然,这在实际中可能并不总是合理的,但只是作为一个例子)。
  • type MappedToNumber<T> = {  
        [K in keyof T]: T[K] extends string ? number : T[K];  
    type Person = {  
        name: string;  
        age: number;  
        hobbies: string[];  
    type PersonWithNumberNames = MappedToNumber<Person>;  
    const person: PersonWithNumberNames = {  
        name: 123, // 这里 name 是一个数字,尽管这在现实中没有意义  
        age: 30,  
        hobbies: ['reading', 'coding']  
    
  • 3.排除类型
    假设你有一个联合类型 T,你可能想要基于某些条件排除其中的某些类型。条件类型可以帮助你实现这一点。
  • 如下示例,使用条件类型来排除一个联合类型中的某个类型:

    type Exclude<T, U> = T extends U ? never : T;  
    type MyUnion = 'a' | 'b' | 'c';  
    type ExcludedUnion = Exclude<MyUnion, 'b'>; // // 结果为 'a' | 'c'
    

    在示例代码中,Exclude<T, U> 是一个条件类型,它检查 T 是否是 U 的子类型。如果是,则返回 never 类型(一个永远不存在的值的类型),否则返回 T 本身。这样,在 ExcludedUnion 类型中,'b' 类型就被排除了。

  • 4.创建新的工具类型
    条件类型经常被用来创建新的工具类型,这些工具类型可以简化其他类型的使用。例如,TypeScript 标准库中的 Exclude<T, U> 和 Extract<T, U> 类型就是基于条件类型构建的。
  • 在 TypeScript 中,Extract 和 Exclude 是两个内置的工具类型,它们在处理类型时非常有用,当然内置的工具类型还有很多。

    Extract<T, U>:
        这个工具类型用于从联合类型 T 中提取出可以赋值给类型 U的类型,生成一个新的类型。
        换句话说,Extract<T, U> 返回一个新类型,该类型是从 T 中提取出来的属于 U 的部分。
        这在类型过滤、类型映射等场景中非常有用。
    Exclude<T, U>:
        这个工具类型用于从联合类型 T 中排除掉类型 U(或 U 的子类型),生成一个新的类型。
        换句话说,Exclude<T, U> 返回一个新类型,该类型是 T 中不属于 U 的部分。
        这在排除特定类型、构建不包含某些类型的联合类型等场景中非常有用。
    

    工具类型源码实现:

    type Exclude<T, U> = T extends U ? never : T;
    type Extract<T, U> = T extends U ? T : never;
    
    type TA = Exclude<"a" | "b" | "c", "a" | "e">; // 结果为 "b" | "c"
    type TB = Extract<"a" | "b" | "c", "a" | "e">; // 结果为 "a"
    type NotNull<T> = Exclude<T, null | undefined>;
    type TC = NotNull<string | number | undefined | null>; // 结果为 string | number
    

    再看一个内置的工具类型:NonNullable,实现:

    type NonNullable<T> = T & {}; // Exclude null and undefined from T
    
    type TD = NonNullable<string | number | undefined | null>; // 结果为 string | number
    
  • 5.处理可选属性
    你可以使用条件类型来检测一个类型是否包含某个可选属性,并基于这个信息返回一个新的类型。
  • type HasOptionalProp<T, K extends keyof T> = K extends keyof T ? (T[K] extends undefined ? false : true) : false;  
    // 示例类型  
    type Example = {  
      a: number;  
      b?: string;  
      c: boolean;  
    // 检查 'b' 是否是 Example 的可选属性  
    type IsBOptional = HasOptionalProp<Example, 'b'>; // 类型为 true  
    

    上面示例中,HasOptionalProp 是一个条件类型,用于检查给定的属性 K 是否是类型 T 的可选属性。

  • 6.实现高级类型逻辑
    条件类型可以与其他高级类型特性(如映射类型、交叉类型、索引类型查询等)结合使用,以实现复杂的类型逻辑。
  • 模块和命名空间

    TypeScript 支持以下两种组织方式来编写代码(即控制代码的作用域):

  • 模块化开发:每个文件可以是一个独立的模块,既支持ES Module,也支持CommonJS
  • 命名空间:通过 namespace 声明一个命名空间。
  • 模块化开发

    模块化开发是指将每个 TypeScript 文件都视为一个独立的模块。

    ES6 模块

    如下示例,封装一个utils/es6/math.ts,其代码如下:

    export function add(n1: number,n2: number) {
        return n1 + n2;
    export function sub(n1: number, n2: number) {
        return n1 - n2;
    

    在 math.ts 文件中定义了 add 和 sub 两个函数,并使用ES Module的规范声明将其导出。因此,math.ts 文件就是一个独立的模块。如果其他文件要使用该模块的功能,就需要先导入该模块的两个函数。

    如下示例,我们有一个 main.js 需要调用上面实现的两个函数。

    import { add, sub } from "./utils/es6/math";
    console.log(add(20, 30));
    console.log(sub(20, 30));
    

    以上代码,在 main.ts 文件中首先导入了 math.ts 模块的 add 和 sub 函数,然后分别打印调用 add 和 sub 函数返回的结果,这就是ES6的模块化开发。

    CommonJS模块

    如下示例,封装一个utils/node/a.node.ts,代码如下:

    let a = {
        x: 1,
    // 整体导出
    module.exports = a
    

    封装一个utils/node/b.node.ts,示例代码如下:

    const c1 = require('./a.node');
    const c2 = require('../es6/math');
    console.log(c1);
    console.log(c2.add(1,2));
    

    在b.node.ts中,我们导入了commonjs模块与es6模块,在导入es6模块是通过require方式导入,当然我们也可以使用 import 方式:

    const c1 = require('./a.node');
    import * as c2 from '../es6/math';
    console.log(c1);
    console.log(c2.add(1,2));
    

    JS 命名空间

    在JavaScript中,命名空间可以帮助我们防止与全局命名空间下的其他对象或变量产生冲突。命名空间也有助于组织代码,有更强的可维护性和可读性。

    以前在SF上看到这样一个提问:

    如题,因为不得已的原因,需要写若干个全局函数。但又不想这样:

    window.a = function(){}
    window.b = function(){}
    window.c = function(){}
    

    题主问有什么好的写法?

    如果你用 jQuery,你可以这样写:

    $.extend(window, {
        a: function() {},
        b: function() {},
        c: function() {}
    

    如果你不用 jQuery,可以直接实现类似的 extend:

    (() => {
        var defining = {
            a: function() { },
            b: function() { },
            c: function() { }
        Object.keys(defining).forEach(key => {
            window[key] = defining[key];
    })();
    

    一般命名空间的实现都以window为根。当向window申请a.b.c的命名空间时,首先在window中查看是否存在a这个成员,如果没有则在 window下新建一个名为a的空关联数组,如果已经存在a,则继续在window.a中查看b是否存在,以此类推。

    JS中,我们一般的实现方式:

    namespace = function(){
        var argus = arguments;
        for(var i = 0; i < argus.length; i++){
            var objs = argus[i].split(".");
            var obj = window;
            for(var j = 0; j < objs.length; j++){
                obj[objs[j]] = obj[objs[j]] || {};
                obj = obj[objs[j]];
        return obj;
    namespace("tools.base");
    

    下面我们看下其他类库的命名空间的实现。

    YUI命名空间的实现方法:

    var YAHOO = window.YAHOO || {};
    YAHOO.namespace = function(ns) {
        if (!ns || !ns.length) {
            return null;
        var levels = ns.split(".");
        var nsobj = YAHOO;
        //如果申请的命名空间是在YAHOO下的,则必须忽略它,否则就成了YAHOO.YAHOO了
        for (var i=(levels[0] == "YAHOO") ? 1 : 0; i<levels.length; ++i) {
            //如果当前命名空间下不存在,则新建一个关联数组。
            nsobj[levels[i]] = nsobj[levels[i]] || {};
            nsobj = nsobj[levels[i]];
        //返回所申请命名空间的一个引用;
        return nsobj;
    

    Atlas命名空间的实现方法(Atlas是由Mozilla开发的一款开源的网页浏览器,它使用自己的名为"Atlas"的命名空间来保护其代码不被外部影响。):

    Function.registerNamespace =function(namespacePath){
        //以window为根
        var rootObject =window;
        //对命名空间路径拆分成数组
        var namespaceParts =namespacePath.split('.');
        for (var i =0;i <namespaceParts.length;i++) {
            var currentPart =namespaceParts[i];
            //如果当前命名空间下不存在,则新建一个Object对象,等效于一个关联数组。
            if(!rootObject[currentPart])      {
               rootObject[currentPart]=new Object();
            rootObject =rootObject[currentPart];
    

    当然JS常用的命名空间模式有如下:

  • 单一全局变量
  • 命名空间前缀
  • 对象字面量表示法
  • 嵌套命名空间
  • 立即调用的函数表达式
  • 命名空间注入
  • 自动嵌套的命名空间
  • ......
  • TS 命名空间

    在 TypeScript 早期,命名空间被称为内部模块,主要用于在一个模块内部进行作用域的划分,防止一些命名冲突的问题。

    如下示例,新建一个utils/format.ts文件,示例代码如下:

    // 声明一个命名空间:Time
    export namespace Time {
        // 在 Time  命名空间中定义属性和方法
        export function format(time: string[]) {
            return time.join('-');
        export let name: string = 'zhangsan';
    // 声明一个命名空间:Price
    export namespace Price {
        export function format(price: number) {
            return price.toFixed(2);
    

    以上,我们声明了 Time 和 Price 两个命名空间并将其导出。

  • 在 Time 命名空间中定义 name 属性和 format 方法,并进行导出。
  • 在 Price 命名空间中定义 format 方法,并进行导出。
  • 我们修改一下调用示例代码main.ts:

    import { Time, Price } from './utils/format';
    console.log(Time.name); // zhangsan
    console.log(Time.format(['2024','04', '30'])); // 2024-04-30
    console.log(Price.format(100.2454)); // 100.25
    

    首先导入了Time 和 Price 命名空间,接着分别调用命名空间中定义的属性和方法。这就是在 TypeScript 中使用命名空间的方式。

    类型的声明

    前面介绍的 TypeScript 类型几乎都是我们自己编写的,但是也使用了一些其他的类型。代码如下所示:

    const imageEl = document.getElementById("image") as HTMLImageElement
    

    为什么在 TypeScript中可以使用HTMLImageElement 类型?这其中就涉及了TypeScript的类型管理和查找规则。

    为了更好地管理类型, TypeScript 提供了另一种文件类型,即 .d.ts 文件(d 是 declare 的缩写)。

    我们之前编写的TypeScript 文件都是.ts 文件,它们在经过构建工具打包之后,最终会输出js文件。.d.ts 文件用于声明 (declare)类型。它仅用于做类型检测,告知 TypeScript 有哪些类型。

    在 TypeScript 中,有三种声明类型的位置:

  • 内置类型声明(如 lib.dom.d.ts)。
  • 1. 内置类型声明
    在 TypeScript 中,内置类型声明是 TypeScript 自带的,它内置了 JavaScript 运行时的一些 标准化 API 的声明文件,其中包括Math 、Date等内置类型以及DOMAPI,如 Window、Document等。

    通常情况下,在安装 TypeScript 时 ,TypeScript 的环境中会自带内置类型声明文件(如lib.dom.d.ts 文件)。

    2. 外部定义类型声明

    外部类型声明通常在使用一些库(如第三方库)时会用到。这些第三方库通常有两种类型声明方式:

  • 1、在自己的库中进行类型声明,比如 axios 的 index.d.ts 文件。
  • 2、使用社区的公有库 DefinitelyTvped 存放类型声明文件。DefinitelyTyped 的 GitHub 地址:https://github.com/DefinitelyTyped/DefinitelyTyped , 当我们需要某个库类型文件时,可执行 npm i @types/xxxx --save-dev 对其进行安装。
  • 比如,当需要安装 jquery 类型声明时,可以在终端执行如下命令:

    npm i @types/jquery --save-dev
    

    TypeScript 编译器会自动在 node_modules/@types 目录下查找相应的类型声明文件。因此,你不需要显式地在你的 TypeScript 文件中使用类似 /// <reference ... /> 的语法来引入它们。

    3. 自定义类型声明

    在开发中,有两种情况需要自定义声明文件:

  • 1、使用的第三方库是一个纯 JavaScript库,没有对应的声明文件,比如 Vue3 中常用的lodash。
  • 2、我们需要在自己的代码中声明一些类型,以便在其他地方直接使用。
  • 注意的是:自定义的声明文件命名可以随便起,该文件一般放在 src 目录下,也可以放到其他目录下,但必须是.d.ts文件,例如shims-vue.d.ts、hy-type.d.ts、global.d.ts 等。

    创建 Vue3 +TypeScript 项目

    前面编写的 TypeScript 代码都是通过 ts-node 运行的,下面介绍 TypeScript代码如何在浏览器中运行。在浏览器中运行 TypeScript 代码,需要借助 webpack 搭建 TypeScript 的运行环境。

    我们下面使用 vuecli 脚手架来搭建TS环境.

  • 1、进入终端环境,执行如下命令:
  • vue create typescript_declare
    
  • 2、选择 “Manually select features“
  • 3、选择“vue3.x 、Babel 、TypeScript” 三个功能
  • 创建好项目后,项目目录结构大致如下:

    typescript_declare/  项目名称
    |-- node_modules         #存放第三方依赖包(例如,执行npm i安装的依赖包)
    |-- public/              #静态资源目录  
    |   |-- favicon.ico      #网站图标  
    |   |-- index.html       #项目的入口文件  
    |-- src/                 #项目的源代码目录  
    |   |-- App.vue          #根组件  
    |   |-- main.ts          #项目的入口文件  
    |   |-- hy-type.d.ts.ts  #将以前的shims-vue.d.ts 文件改成hy-type.d.ts文件(可以随意命名)
    |-- babel.config.js      #Babel 插件的配置文件 
    |-- package-lock.json    #npm 依赖的锁定文件  
    |-- package.json         #项目的元数据文件和 npm 脚本  
    |-- README.md            #项目的说明文件 
    |-- tsconfig.json        #ts配置文件
    

    我们调整下APP.vue代码:

    <template>
      <div>Hello TypeScript</div>
    </template>
    <script lang="ts">
    import { defineComponent } from 'vue';
    export default defineComponent({
      name: 'App'
    </script>
    

    main.js代码如下:

    import { createApp } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')
    

    tsconfig.json配置文件:

    "compilerOptions": { // 编译器的配置项 "target": "es5", // 目标代码 "module": "commonjs", // 生成代码使用的模块化 "strict": true, // 启用严格模式 "skipLibCheck": true, // 跳过整个库进行类型检查,只检查用到的类型 "esModuleInterop": true, // 让ES Module 和 commonjs 相互调用 "forceConsistentCasingInFileNames": true, // 强制使用大小写一致的文件名

    declare 声明变量

    在 TypeScript中,为了声明全局变量,需要使用 declare 关键字。

    在上面构建的工程中的public/index.html中定义全局变量,代码如下:

    <div id="app"></div> <script> // 定义全局变量 const appName = 'vue3+ts'; const appVersion = '1.0'; </script>

    然后我们在项目src/main.ts中使用 appName 和 appVersion 两个全局变量,代码如下:

    import { createApp } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')
    // 使用全局变量
    console.log(appName); // 报错:Cannot find name 'appName'.
    console.log(appVersion); // 报错:Cannot find name 'appVersion'.
    

    可以看到,在 main.ts 中直接使用 appName 和 appVersion 两个全局变量会提示报错,因为这两个变量并没有全局声明类型。

    为了让这两个全局变量能够全局使用而不报错,我们需要对这两个变量进行全局声明。修改项目中 src/hy-type.d.ts 文件(自定义声明文件),代码如下:

    // 声明全局变量,告诉编译器该变量已声明了 declare const appName: string; declare const appVersion: string;

    这样,在src/main.ts就不会报错了。

    declare 声明函数

    在 TypeScript中,声明全局函数也需要用到 declare 关键字。

    在 public/index.html 文件中定义全局函数,代码如下所示:

    // 定义全局变量
    // 定义全局函数
    function getAppName() {
    return appName;
    

    接下来,我们修改 src/hy-type.d.ts 文件代码,对 getAppName 函数进行全局声明,代码如下:

    // 声明全局函数,告诉编译器该函数已声明了 // declare const getAppName: () => void; declare function getAppName(): void;

    然后,在 src/main.ts 中使用 getAppName 全局函数,代码如下所示:

    // 使用全局函数 console.log(getAppName()); // 正确执行

    declare 声明类

    在 TypeScript中,声明全局类也需要使用到 declare 关键字。在 public/index.html 文件中定义全局类,代码如下:

    // 定义全局类 function Person(name,age) { this.name = name; this.age = age;

    接着,修改 src/hy-type.d.ts 文件,对 Person 类进行全局声明,代码如下:

    // 声明全局类 declare class Person { name: string; age: number; constructor(name: string, age: number);

    最后,在src/main.ts 文件中使用 Person 全局类,代码如下:

    // 使用全局类 const p = new Person('张三', 18); console.log(p);

    declare 声明文件

    在前端开发中,需要导入各种文件,例如图片、Vue3 文件等。为了正确地声明导入的文件,需要用到 declare 关键字。

    我们在 src/img 下面存放一张图片 name.jpg。接着修改 src/hy-type.d.ts 文件,对需要导入的.jpg 、.jpeg 、.png 等文件进行全局声明,代码如下:

    // 声明导入.jpg、.jpeg、.png、.gif、.svg等文件
    declare module '*.jpg';
    declare module '*.jpeg';
    declare module '*.png';
    declare module '*.gif';
    declare module '*.svg';
    

    然后,在 src/main.ts 中导入name.jpg 文件,代码如下:

    import App from './App.vue'
    // 导入文件
    import nameImg from './img/name.jpg';
    createApp(App).mount('#app')
    .....
    

    declare 声明模块

    TypeScript 支持通过模块化和命名空间这两种方式来组织代码。在使用模块化时,需要使用 declare 关键字声明导入的模块。

    下面以导入 lodash 模块的为例,我们先在工程下安装lodash,执行命令:

    npm i lodash -S
    

    接着,在 main.ts 中导入,代码如下:

    .....
    import nameImg from './img/name.jpg';
    import lodash from 'lodash'; // 报错:Could not find a declaration file for module 'lodash'
    

    可以看到,安装完 lodash 模块后,在 main.ts 导入 lodash 时会提示找不到该模块的声明文件的错误。

    这是因为安装 lodash 模块并没有对应的声明文件。这个时候,可以执行 npm i @types/lodash --save-dev 命令,安装它的声明文件,或手动编写该模块的声明文件。

    下面讲解如何手动编写 lodash 模块的声明。声明模块的具体语法如下:

    declare module '模块名' { export  xxx}
    

    意思是用 declare module 声明一个模块。在模块内部,需要使用 export 导出对应库的类、函数等。

    接着,我们修改 src/hy-type.d.ts 文件,添加对 lodash 模块的全局声明,代码如下:

    // 声明导入的模块
    declare module 'lodash' {
      export function join(args: any[]): any; //  声明模块中有一个join 函数,即lodash.join()
      // 可以继续导出 lodash 的其他方法
    

    这样,之前 src/main.ts 文件中报错的代码就不会再报错了。

    declare 声明命名空间

    在 TypeScript 中声明命名空间和声明模块的方式与 TypeScript 类似,也需要使用 declare 关键字。

    在“public/index.html ”文件中导入 iQuery 库,代码如下:

    <!--执行下面的代码会在 window 上添加 jQuery 和 $ 属性,它们都是 jQuery 函数-->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
    

    接着,为了在全局中直接使用 $ 函数,可以对 $ 函数进行命名空间的声明。src/hy-type.d.ts 代码如下所示:

    // 声明 $ 命名空间
    declare namespace $ {
      function ajax(settins: any): void;
    

    这样就可以在main.ts中使用 $ 全局函数了,示例代码如下:

    //  全局使用 $ 函数不会提示报错
    $.ajax({
        url: 'http://www.baidu.com',
        success(data:any) {
            console.log(data);
    

    类型声明分类

    以下类型声明分类,建议在自定义脚手架上验证,更接近工程配置。

    global 类型声明

    示例,我们在工程static文件下新建一个global-lib.js文件,示例代码如下:

    function globalLib(options) {
      console.log(options);
    globalLib.version = "1.0.0";
    globalLib.doSomething = function () {
      console.log("globalLib do something");
    

    我们在入口文件index.ts或者main.ts中调用上述方法:

    console.log(globalLib.version);
    globalLib.doSomething();
    

    当然肯定会报错,找不到名称“globalLib”,我们需要声明,新建文件utils/global-lib.d.ts:

    declare function globalLib(options: globalLib.Options): void;
    declare namespace globalLib {
        const version: string;
        function doSomething(): void;
        interface Options {
            [key: string]: any
    

    module 类型声明

    如果你的ES模块是用TypeScript编写的,并且你在同一个项目中直接使用它,那么通常你不需要额外的类型声明文件(.d.ts),因为TypeScript编译器会自动从.ts或.tsx文件中生成类型信息。
    例如我们在utils下新建一个js文件,utils/module-lib.js,示例代码:

    export const version = "1.0.0";
    export function doSomething() {
      console.log("moduleLib do something");
    

    编写类型申明文件,utils/module-lib.d.ts:

    declare namespace moduleLib {
        const version: string;
        function doSomething(): void;
    export = moduleLib;
    

    我们现在在入口文件main.ts中导入module-lib.js模块:

    import { version, doSomething } from './utils/module-lib';
    console.log(version);
    doSomething()
    

    这样就不会报错了。

    UMD 类型声明

    在 Vue 工程中引入 UMD(Universal Module Definition)文件,通常是为了使用某个库或组件,而这个库或组件是以 UMD 格式发布的。UMD 是一种模块定义方式,它允许你的代码在多种环境中运行,包括 CommonJS、AMD 和全局变量环境。如果当前引入的UMD文件没有进行类型声明,我们就要对它进行手写类型声明文件。

    在工程中新建一个UMD文件,utils/umd-lib.js,示例代码如下:

    (function (root, factory) {
      if (typeof define === "function" && define.amd) {
        define(factory);
      } else if (typeof module === "object" && module.exports) {
        module.exports = factory();
      } else {
        root.umdLib = factory();
    })(this, function () {
      return {
        version: "1.0.0",
        doSomething() {
          console.log("umdLib do something");
    

    然后我们在入口文件导入该UMD文件,并调用文件内的方法:

    import umdLib from './utils/umd-lib';
    console.log(umdLib.version);
    umdLib.dosomething();
    

    当然到这儿肯定会报错,我们没有编写对应的类型声明,我们在utils/umd-lib.d.ts文件:

    declare namespace umdLib {
        const version: string;
        function doSomething(): void;
    export = umdLib;
    

    附:工程配置及源码

    工程目录:

    + |- /app-build
        + |- webpack.base.config.js
        + |- webpack.config.js
        + |- webpack.dev.config.js
        + |- webpack.pro.config.js
    + |- /src
        + |- utils
            + |- global-lib.d.ts
            + |- module-lib.d.ts
            + |- module-lib.js
            + |- umd-lib.d.ts
            + |- umd-lib.js
        + |- index.ts
    + |- /static
        + |- global-lib.js
    + |- index.html
    + |- package.json
    + |- tsconfig.json
    + |- README.md
    

    app-build/webpack.base.config.js:

    const HtmlWebpackPlugin = require("html-webpack-plugin");
    module.exports = {
      entry: "./src/index.ts",
      output: {
        filename: "app.js",
      resolve: {
        extensions: [".js", ".ts", ".tsx"],
      module: {
        rules: [
            test: /\.js$/i,
            use: [
                loader: "babel-loader",
            exclude: /node_modules/,
            test: /\.tsx?$/i,
            use: [
                loader: "ts-loader",
            exclude: /node_modules/,
      plugins: [
        new HtmlWebpackPlugin({
          template: "./index.html",
    

    app-build/webpack.config.js:

    const { merge } = require("webpack-merge");
    const baseConfig = require("./webpack.base.config");
    const devConfig = require("./webpack.dev.config");
    const proConfig = require("./webpack.pro.config");
    module.exports = (env, argv) => {
      let config = argv.mode === "development" ? devConfig : proConfig;
      return merge(baseConfig, config);
    

    app-build/webpack.dev.config.js:

    const path = require("path");
    const CopyWebpackPlugin = require("copy-webpack-plugin");
    module.exports = {
      devtool: "eval-cheap-module-source-map",
      devServer: {
        headers: { "Access-Control-Allow-Origin": "*" },
        historyApiFallback: {
          rewrites: [
              from: /.*/,
              to: path.posix.join("/", "index.html"),
        static: {
          directory: path.resolve(__dirname, "../dist"),
          publicPath: "/",
        hot: true,
        compress: true,
        client: {
          // 关闭webpack-dev-server客户端的日志输出
          logging: "none",
          overlay: {
            // compilation errors
            errors: true,
            // compilation warnings
            warnings: false,
            // unhandled runtime errors
            runtimeErrors: false,
          // 显示打包进度
          progress: true,
        host: "localhost",
        port: 8091,
        open: true,
      plugins: [
        new CopyWebpackPlugin({
          patterns: [
            { from: "static", to: "static" }, // 将 static 目录下的文件复制到 dist/static
    

    app-build/webpack.pro.config.js:

    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    module.exports = {
      plugins: [new CleanWebpackPlugin()],
    

    src/utils/global-lib.d.ts: