TypeScript 学习纪要

2021-05-11  本文已影响0人  percivals

TypeScript

[toc]

什么是 TypeScript?

安装

TypeScript 的命令行工具安装方法如下:

npm install -g typescript

以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了。

编译一个 TypeScript 文件很简单:

tsc hello.ts

我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx为后缀。

主流的编辑器都支持 TypeScript,这里我推荐使用 Visual Studio Code

使用基础

  1. 字符串

    `Hello, my name is ${myName}.`
    

    等同于js的下述语句,其中${expr} 用来在模板字符串中嵌入表达式。

    "Hello, my name is " + myName + ".
    
  2. Any 任意值类型

    允许被赋值为任意类型,在任意值上访问任何属性都是允许的,也允许调用任何方法

    let something;
    something = 'seven';
    something = 7;
    
    something.setName('Tom');
    

    编译不会报错,但是如果逻辑未实现,运行依旧会报错

  3. 类型推论

    TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

    如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

  4. 联合类型

    联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型

    let myFavoriteNumber: string | number;
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;
    
  5. 对象的类型

    在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

    interface Person {
        name: string;
        age: number;
    }
    
    let tom: Person = {
        name: 'Tom',
        age: 25
    };
    

    有时我们希望不要完全匹配一个形状,那么可以用可选属性:

    interface Person {
        name: string;
        age?: number;
    }
    
    let tom: Person = {
        name: 'Tom'
    };
    

    有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

    interface Person {
        name: string;
        age?: number;
        [propName: string]: any;
    }
    
    let tom: Person = {
        name: 'Tom',
        gender: 'male'
    };
    

    需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

    interface Person {
        name: string;
        age?: number;
        [propName: string]: string;
    }
    

    上述这种写法会报错,任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

    一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

    interface Person {
        name: string;
        age?: number;
        [propName: string]: string | number;
    }
    
    let tom: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'
    };
    

    只读属性,有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性,该属性不可再次赋值

    interface Person {
        readonly id: number;
        name: string;
        age?: number;
        [propName: string]: any;
    }
    
    let tom: Person = {
        id: 89757,
        name: 'Tom',
        gender: 'male'
    };
    
  6. 数组

    在 TypeScript 中,数组类型有多种定义方式,比较灵活。

    最简单的方法是使用「类型 + 方括号」来表示数组,数组的项中不允许出现其他的类型:

    let fibonacci: number[] = [1, 1, 2, 3, 5];
    

    我们也可以使用数组泛型(Array Generic) Array<elemType> 来表示数组:

    let fibonacci: Array<number> = [1, 1, 2, 3, 5];
    
  7. 函数的类型

    函数声明的类型定义:

    function sum(x: number, y: number): number {
        return x + y;
    }
    

    注意,输入多余的(或者少于要求的)参数,是不被允许的

    通过可选参数,可以实现输入多余或少于要求的参数

    function buildName(firstName: string, lastName?: string) {
        if (lastName) {
            return firstName + ' ' + lastName;
        } else {
            return firstName;
        }
    }
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName('Tom');
    

    需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

    参数默认值:允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:

    function buildName(firstName: string, lastName: string = 'Cat') {
        return firstName + ' ' + lastName;
    }
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName('Tom');
    

    剩余参数

    ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数),注意,rest 参数只能是最后一个参数:

    function push(array, ...items) {
        items.forEach(function(item) {
            array.push(item);
        });
    }
    
    let a: any[] = [];
    push(a, 1, 2, 3);
    

    重载

    我们可以使用重载定义多个 reverse 的函数类型:

    function reverse(x: number): number;
    function reverse(x: string): string;
    function reverse(x: number | string): number | string {
        if (typeof x === 'number') {
            return Number(x.toString().split('').reverse().join(''));
        } else if (typeof x === 'string') {
            return x.split('').reverse().join('');
        }
    }
    

    上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

    注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

  8. 类型断言

    值 as 类型
    

    类型断言的常见用途有以下几种:

    • 联合类型可以被断言为其中一个类型
    • 父类可以被断言为子类
    • 任何类型都可以被断言为 any
    • any 可以被断言为任何类型
    • 要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可
  9. 声明文件 https://ts.xcatliu.com/basics/declaration-files.html#declare-var

    1)通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件,声明文件必需以 .d.ts 为后缀。

    一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。
    
    /path/to/project
    ├── src
    |  ├── index.ts
    |  └── jQuery.d.ts
    └── tsconfig.json
    
    假如仍然无法解析,那么可以检查下 tsconfig.json 中的 files、include 和 exclude 配置,确保其包含了 jQuery.d.ts 文件。
    

    2)第三方声明文件

    当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped。
    我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。
    @types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
    npm install @types/jquery --save-dev
    可以在这个页面搜索你需要的声明文件
    

    3)书写声明文件

    当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。

进阶知识

  1. 类型别名

    我们使用 type 创建类型别名,类型别名常用于联合类型

    type Name = string;
    type NameResolver = () => string;
    type NameOrResolver = Name | NameResolver;
    function getName(n: NameOrResolver): Name {
        if (typeof n === 'string') {
            return n;
        } else {
            return n();
        }
    }
    
  2. 字符串字面量类型

    字符串字面量类型用来约束取值只能是某几个字符串中的一个。

    type EventNames = 'click' | 'scroll' | 'mousemove';
    function handleEvent(ele: Element, event: EventNames) {
        // do something
    }
    
    handleEvent(document.getElementById('hello'), 'scroll');  // 没问题
    handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'
    
    // index.ts(7,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'.
    

    上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

    注意,类型别名与字符串字面量类型都是使用 type 进行定义。

  3. 元组

    数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

    元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。

    let tom: [string, number] = ['Tom', 25];
    

    当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

    let tom: [string, number];
    tom = ['Tom', 25];
    tom.push('male');
    
  4. 枚举

    枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

    枚举使用 enum 关键字来定义:

    enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
    

    枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

    enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
    
    console.log(Days["Sun"] === 0); // true
    console.log(Days["Mon"] === 1); // true
    console.log(Days["Tue"] === 2); // true
    console.log(Days["Sat"] === 6); // true
    
    console.log(Days[0] === "Sun"); // true
    console.log(Days[1] === "Mon"); // true
    console.log(Days[2] === "Tue"); // true
    console.log(Days[6] === "Sat"); // true
    

    我们也可以给枚举项手动赋值:

    enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
    
    console.log(Days["Sun"] === 7); // true
    console.log(Days["Mon"] === 1); // true
    console.log(Days["Tue"] === 2); // true
    console.log(Days["Sat"] === 6); // true
    
  5. 1)属性和方法

    使用 class 定义类,使用 constructor 定义构造函数

    通过 new 生成新实例的时候,会自动调用构造函数。

    2)类的继承

    使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法

    class Cat extends Animal {
      constructor(name) {
        super(name); // 调用父类的 constructor(name)
        console.log(this.name);
      }
      sayHi() {
        return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
      }
    }
    
    let c = new Cat('Tom'); // Tom
    console.log(c.sayHi()); // Meow, My name is Tom
    

    3)静态方法

    使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

    4)TypeScript中类的用法

    TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。
    public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
    private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
    protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
    
  6. 类与接口

    实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

    interface Alarm {
        alert(): void;
    }
    
    class Door {
    }
    
    class SecurityDoor extends Door implements Alarm {
        alert() {
            console.log('SecurityDoor alert');
        }
    }
    
    class Car implements Alarm {
        alert() {
            console.log('Car alert');
        }
    }
    

    一个类可以实现多个接口:

    interface Alarm {
        alert(): void;
    }
    
    interface Light {
        lightOn(): void;
        lightOff(): void;
    }
    
    class Car implements Alarm, Light {
        alert() {
            console.log('Car alert');
        }
        lightOn() {
            console.log('Car light on');
        }
        lightOff() {
            console.log('Car light off');
        }
    }
    

    接口继承接口

    接口与接口之间可以是继承关系:

    interface Alarm {
        alert(): void;
    }
    
    interface LightableAlarm extends Alarm {
        lightOn(): void;
        lightOff(): void;
    }
    

    这很好理解,LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,还拥有两个新方法 lightOnlightOff

    接口继承类

    常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:

    class Point {
        x: number;
        y: number;
        constructor(x: number, y: number) {
            this.x = x;
            this.y = y;
        }
    }
    
    interface Point3d extends Point {
        z: number;
    }
    
    let point3d: Point3d = {x: 1, y: 2, z: 3};
    

    为什么 TypeScript 会支持接口继承类呢?

    实际上,当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

    所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例)

    也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型)

    class Point {
        x: number;
        y: number;
        constructor(x: number, y: number) {
            this.x = x;
            this.y = y;
        }
    }
    
    interface PointInstanceType {
        x: number;
        y: number;
    }
    
    function printPoint(p: PointInstanceType) {
        console.log(p.x, p.y);
    }
    
    printPoint(new Point(1, 2));
    

    声明 Point 类时创建的 Point 类型只包含其中的实例属性和实例方法,不包含构造函数、静态属性、静态方法等

  7. 泛型

    泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

    function createArray<T>(length: number, value: T): Array<T> {
        let result: T[] = [];
        for (let i = 0; i < length; i++) {
            result[i] = value;
        }
        return result;
    }
    
    createArray<string>(3, 'x'); // ['x', 'x', 'x']
    

    定义泛型的时候,可以一次定义多个类型参数:

    function swap<T, U>(tuple: [T, U]): [U, T] {
        return [tuple[1], tuple[0]];
    }
    
    swap([7, 'seven']); // ['seven', 7]
    

    泛型约束

    interface Lengthwise {
        length: number;
    }
    
    function loggingIdentity<T extends Lengthwise>(arg: T): T {
        console.log(arg.length);
        return arg;
    }
    

    上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

参考:https://ts.xcatliu.com/engineering/lint.html

上一篇下一篇

猜你喜欢

热点阅读