TS

289 阅读9分钟

1 TypeScript 简介

  • JS 属于动态编程语言,而 TS 属于静态编程语言

  • JS 是边解释边执行,错误只能在运行的时候才被发现

  • TS 是先编译再执行,在写的时候就会发现错误

  • 类型注解: 定义变量并为之规定类型,如 const x:number,即告诉TS变量是什么类型.

  • 类型推断: 定义变量并赋值但不为之规定类型,如 const x= 1,TS则自动尝试分析变量类型。

  • 类型断言: 类型断言不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

    类型断言有两种形式。 其一是“尖括号”语法:

    let someValue: any = "this is a string";
      
    let strLength: number = (<string>someValue).length;
    

    另一个为as语法:

    let someValue: any = "this is a string";
      
    let strLength: number = (someValue as string).length;
    

    两种形式是等价的, 但当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。

2 基础类型

  • 布尔值: let isDone: boolean = false;

  • 数字: let decLiteral: number = 6;

  • 字符串: let name: string = "bob";

    还可以使用模版字符串,它可以定义多行文本和内嵌表达式。

    let name: string = `Gene`;
    
    let age: number = 37;
    
    let sentence: string = `Hello, my name is ${ name }.
    
    I'll be ${ age + 1 } years old next month.`;
    
  • 数组:

    let list: number[] = [1, 2, 3];
    
    let list: Array<number> = [1, 2, 3];
    
  • 元组:

    表示一个已知 元素数量 和 类型 的数组,各元素的类型不必相同。

    let x: [string, number];
    x = ['hello'10]; // OK
    x = [10'hello']; // Error
    
    当访问一个已知索引的元素,会得到正确的类型:
    console.log(x[0].substr(1)); // OK
    console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
    当访问一个越界的元素,会使用联合类型替代:
    
    x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型;
    console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString;
    x[6] = true; // Error, 布尔不是(string | number)类型;
    
  • 枚举: 对标准数据类型的一个补充

    可以为一组数值赋予友好的名字:
    enum Color {Red, Green, Blue};
    let c: Color = Color.Green;
    
    默认情况下,从0开始为元素编号, 可以手动的指定成员的数值。 
    enum Color {Red = 1, Green, Blue}
    let c: Color = Color.Green;
    
    或者,全部都采用手动赋值:
    enum Color {Red = 1, Green = 2, Blue = 4}
    let c: Color = Color.Green;
    
    可以由枚举的值得到它的名字:
    enum Color {Red = 1, Green, Blue}
    let colorName: string = Color[2];
    console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
    
  • Any: 避开类型检查,直接让它们通过编译阶段的检查

    let notSure: any = 4;
    notSure = "maybe a string instead";
    notSure = false; // okay, definitely a boolean
    

    Object有相似的作用, 但 Object 类型的变量只允许给它赋任意值, 不允许在它上面调用任意的方法, 即便它真的有这些方法.

  • Void: 表示没有任何类型, 当一个函数没有返回值时,通常会见到其返回值类型是 void:

    function warnUser(): void {
        console.log("This is my warning message");
    }
    

    声明一个void类型的变量,只能为它赋予undefined和null: let unusable: void = undefined;

  • Null 和 Undefined: undefined和null两者 各自有自己的类型 分别叫做 undefined 和 null。

    let u: undefined = undefined;
    let n: null = null;
    

    默认情况下,null 和 undefined 是所有类型的子类型。

    当指定了--strictNullChecks标记,null 和 undefined 只能赋值给 void 和它们各自。

  • Never: 表示的是那些永不存在的值的类型。

    例如, never类型是那些总是会 抛出异常 或 根本就 不会有 返回值的 函数表达式 或 箭头函数表达式 的返回值类型;

    变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

    never类型是任何类型的子类型,也可以赋值给任何类型;

    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
        throw new Error(message);
    }
    
    // 推断的返回值类型为never
    function fail() {
        return error("Something failed");
    }
    
    // 返回never的函数必须存在无法达到的终点
    function infiniteLoop(): never {
        while (true) {
        }
    }
    
  • Object: object表示非原始类型,也就是除number,string,boolean,symbol,null 或 undefined 之外的类型。

3 接口

  • interface:定义通用性的类型,通常为对象,可以将对象的公共属性封装到接口中,接口也可以继承(extents)。

  • Implements:类应用接口,类必须具有接口中的所有属性和方法。

  • 3.1 接口初探:

    下面通过一个简单示例来观察接口是如何工作的:

    interface LabelledValue {
        label: string;
    }
    
    function printLabel(labelledObj: LabelledValue) {
        console.log(labelledObj.label);
    }
    
    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);
    

    类型检查器会查看 printLabel 的调用。 printLabel 有一个参数,并要求这个对象参数有一个名为label类型为string的属性。

    注意: 传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在, 并且其类型是否匹配,只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

    类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

  • 3.2 可选属性

    在可选属性名字定义的后面加一个?符号。

    好处是:可以对可能存在的属性进行预定义;可以捕获引用了不存在的属性时的错误。

    interface SquareConfig {
      color?: string;
      width?: number;
    }
    
    function createSquare(config: SquareConfig): {color: string; area: number} {
      let newSquare = {color: "white", area: 100};
      if (config.color) {
          newSquare.color = config.color;
      }
      if (config.width) {
          newSquare.area = config.width * config.width;
      }
      return newSquare;
    }
    
    let mySquare = createSquare({color: "black"});
    
  • 3.3 只读属性

    用readonly来指定只读属性

    interface Point {
      readonly x: number;
      readonly y: number;
    }
    
    let p1: Point = { x: 10, y: 20 };
    p1.x = 5; // error!
    

    TypeScript 具有 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

    let a: number[] = [1, 2, 3, 4];
    let ro: ReadonlyArray<number> = a;
    ro[0] = 12; // error!
    ro.push(5); // error!
    ro.length = 100; // error!
    a = ro; // error!
    

    上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。

    但是你可以用类型断言重写:a = ro as number[];

    readonly vs const: 做为变量使用的话用 const,若做为属性则使用readonly。

  • 3.4 额外的属性检查

    //  error: 'colour' not expected in type 'SquareConfig'
    

    let mySquare = createSquare({ colour: "red", width: 100 });

    3.4.1 绕开这些检查, 最简便的方法是使用类型断言:

    let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

    3.4.2 最佳的方式是添加一个字符串索引签名:

    interface SquareConfig {
      color?: string;
      width?: number;
      [propName: string]: any;
    }
    

    3.4.3 还有最后一种跳过这些检查的方式,就是将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错。

    let squareOptions = { colour: "red", width: 100 };
    let mySquare = createSquare(squareOptions);
    
  • 3.5 函数类型

    除了描述带有属性的普通对象外,接口也可以描述函数类型。

    3.5.1 使用接口表示函数类型,需要给接口定义一个调用签名。

      interface SearchFunc {
          (source: string, subString: string): boolean;
      }
      
      let mySearch: SearchFunc;
      
      mySearch = function(source: string, subString: string) {
          let result = source.search(subString);
          return result > -1;
      }
      
    

    3.5.2 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。

      let mySearch: SearchFunc;
      
      mySearch = function(src: string, sub: string): boolean {
         let result = src.search(sub);
          return result > -1;
      }
      
    

    3.5.3 函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。

    3.5.4 如果不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。

    3.5.5 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。

    3.5.6 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。

     let mySearch: SearchFunc;
     
     mySearch = function(src, sub) {
          let result = src.search(sub);
          return result > -1;
      }
    
  • 3.6 可索引的类型

    可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

    interface StringArray {
       [index: number]: string;
    }
    
    let myArray: StringArray;
    myArray = ["Bob""Fred"];
    
    let myStr: string = myArray[0];
    

    TypeScript支持两种索引签名:字符串和数字, 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。

    这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。

    class Animal {
        namestring;
    }
    class Dog extends Animal {
        breedstring;
    }
    
    // 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
    interface NotOkay {
        [xnumber]: Animal;
        [xstring]: Dog;
    }
    
    interface NumberDictionary {
    [indexstring]: number;
    lengthnumber;    // 可以,length是number类型
    namestring       // 错误,`name`的类型与索引类型返回值的类型不匹配
    }
    

    可以将索引签名设置为只读,这样就防止了给索引赋值:

    interface ReadonlyStringArray {
      readonly [index: number]: string;
    }
    let myArray: ReadonlyStringArray = ["Alice""Bob"];
    myArray[2] = "Mallory"; // error!
    
  • 3.7 类类型---实现接口

    接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

    interface ClockInterface {
        currentTime: Date;
    }
    
    class Clock implements ClockInterface {
        currentTime: Date;
        constructor(h: number, m: number) { }
    }
    
    也可以在接口中描述一个方法,在类里实现它:
    
    interface ClockInterface {
        currentTime: Date;
        setTime(d: Date);
    }
    
    class Clock implements ClockInterface {
        currentTime: Date;
        setTime(d: Date) {
            this.currentTime = d;
        }
        constructor(h: number, m: number) { }
    }
    

    类静态部分与实例部分的区别:

    当操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型 和 实例的类型。
    
    你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
    
    interface ClockConstructor {
        new (hour: number, minute: number);
    }
    
    class Clock implements ClockConstructor {
        currentTime: Date;
        constructor(h: number, m: number) { }
    }
    
    因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 
    constructor存在于类的静态部分,所以不在检查的范围内。
    

    因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。

    interface ClockConstructor {
        new (hour: number, minute: number): ClockInterface;
    }
    interface ClockInterface {
        tick();
    }
    
    function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
        return new ctor(hour, minute);
    }
    
    class DigitalClock implements ClockInterface {
        constructor(h: number, m: number) { }
        tick() {
            console.log("beep beep");
        }
    }
    class AnalogClock implements ClockInterface {
        constructor(h: number, m: number) { }
        tick() {
            console.log("tick tock");
        }
    }
    
    let digital = createClock(DigitalClock, 12, 17);
    
    let analog = createClock(AnalogClock, 7, 32);
    

    继承接口: 让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

    interface Shape {
        color: string;
    }
    
    interface Square extends Shape {
        sideLength: number;
    }
    
    let square = <Square>{};
    square.color = "blue";
    square.sideLength = 10;
    
    一个接口可以继承多个接口,创建出多个接口的合成接口。
    
    interface Shape {
        color: string;
    }
    
    interface PenStroke {
        penWidth: number;
    }
    
    interface Square extends Shape, PenStroke {
        sideLength: number;
    }
    
    let square = <Square>{};
    square.color = "blue";
    square.sideLength = 10;
    square.penWidth = 5.0;