欢迎来到我的博客

vuePress-theme-reco hbbaly    2021
欢迎来到我的博客

Choose mode

  • dark
  • auto
  • light
首页
时间轴
标签
分类
  • 前端
GitHub
author-avatar

hbbaly

31

Article

18

Tag

首页
时间轴
标签
分类
  • 前端
GitHub
  • 【TS】TypeScript入门

    • 基础类型
      • 布尔值 boolean
      • 数字 number
      • 字符串 string
      • 数组
      • 元组 Tuple
      • 枚举 enum
      • Any
      • Void
      • Null && Undefined
      • Never
      • Object
    • 类型断言
      • 接口 Interface
        • 可选属性
        • 只读属性
        • 额外的属性检查
        • 函数类型
        • 可索引的类型
        • 继承接口
        • 混合类型
      • 类 Class
        • 继承
        • public
        • private
        • protected
        • readonly
        • 参数属性
        • 存取器
        • 静态属性
      • 函数
        • 可选参数
        • 默认参数
        • 剩余参数
      • 泛型
        • 类型变量
        • 使用泛型变量
        • 泛型类型
        • 泛型类
        • 泛型约束
      • 类型推论
        • 最佳通用类型
        • 上下文类型
        • 类型兼容性
        • 比较两个函数
        • 枚举
        • 类
        • 泛型

    【TS】TypeScript入门

    vuePress-theme-reco hbbaly    2021

    【TS】TypeScript入门


    hbbaly 2020-03-20 20:29:15 TS

    # 基础类型

    TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

    # 布尔值 boolean

    最基本的数据类型就是简单的true/false值。

    let isDone: boolean = false
    

    # 数字 number

    TypeScript里的所有数字都是浮点数。

    除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

    let decLiteral: number = 6;
    let hexLiteral: number = 0xf00d;
    let binaryLiteral: number = 0b1010;
    let octalLiteral: number = 0o744;
    

    # 字符串 string

    可以使用双引号( ")或单引号(')表示字符串

    let name: string = "bob";
    

    使用模版字符串

    let name: string = `bob${decLiteral}`
    

    # 数组

    有两种方式可以定义数组

    • 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组
    let list: number[] = [1, 2, 3]
    
    • 第二种方式是使用数组泛型,Array<元素类型>
    let list: Array<number> = [1, 2, 3]
    

    # 元组 Tuple

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

    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'
    

    访问越界元素

    3.1及之后的版本不能访问, 之前的版本不报错

    3.1版本之后, Tuple 的定义已经变成了有限制长度的数组,不能进行越界访问。 但是能进行例如 push 的操作, 但是不能访问超出边界,即使push了,边界没有变的。

    x.push('hbb')  // ['hello', 10, 'hbb']
    x.length // 3
    x[2]  // Tuple type '[string, number]' of length '2' has no element at index '2'
    

    # 枚举 enum

    enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

    enum Color {Red, Green, Blue}
    let c: Color = Color.Green  // 1
    

    默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。

    enum Color {Red = 1, Green, Blue}
    let c: Color = Color.Green  // 2
    

    全部都采用手动赋值:

    enum Color {Red = 1, Green = 2, Blue = 4}
    let c: Color = Color.Green // 2
    Color[0]  // undefined
    

    枚举类型提供的一个便利是你可以由枚举的值得到它的名字。

    enum Color {Red = 1, Green, Blue}
    let colorName: string = Color[2]
    
    console.log(colorName)  // 显示'Green'因为上面代码里它的值是2
    

    # Any

    避免ts进行类型检查。

    let notSure: any = 4
    notSure = "maybe a string instead"
    notSure = false  // okay, definitely a boolean
    
    let notSure: any = 4
    notSure.ifItExists() // okay, ifItExists might exist at runtime, 编译通过,运行不通过
    notSure.toFixed() // okay, toFixed exists (but the compiler doesn't check)
    
    let prettySure: Object = 4
    prettySure.toFixed() // Error: Property 'toFixed' doesn't exist on type 'Object'.
    

    当你只知道一部分数据的类型时,any类型也是有用的。

    let list: any[] = [1, true, "free"]
    
    list[1] = 100
    

    # Void

    void类型像是与any类型相反,它表示没有任何类型。

    当一个函数没有返回值时,你通常会见到其返回值类型是 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是所有类型的子类型

    let u: undefined = undefined
    u = 1
    

    编译时加上 --strictNullChecks, null和undefined只能赋值给void和它们各自。

    tsc index.ts --strictNullChecks
    

    # Never

    never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

    never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给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之外的类型。

    declare function create(o: object | null): void;
    
    create({ prop: 0 }) // OK
    create(null) // OK
    
    create(42) // Error
    create("string") // Error
    create(false) // Error
    create(undefined) // Error
    

    # 类型断言

    类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

    类型断言有两种形式。

    • 其一是“尖括号”语法
    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语法断言是被允许的。

    # 接口 Interface

    TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

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

    LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

    # 可选属性

    接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

    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"});
    

    可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

    # 只读属性

    一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性。

    interface Point {
      readonly x: number
      readonly y: number
    }
    

    赋值后, x和y再也不能被改变了

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

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

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

    # 额外的属性检查

    interface SquareConfig {
        color?: string
        width?: number
    }
    
    function createSquare(config: SquareConfig): { color: string; area: number } {
    }
    
    let mySquare = createSquare({ colour: "red", width: 100 })
    

    TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

    // error: 'colour' not expected in type 'SquareConfig'
    let mySquare = createSquare({ colour: "red", width: 100 });
    

    绕开这些检查非常简单。 最简便的方法是使用类型断言

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

    如果 SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它

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

    但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。

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

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

    # 函数类型

    接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

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

    它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

    interface SearchFunc {
      (source: string, subString: string): boolean;
    }
    

    函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。

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

    # 可索引的类型

    与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或ageMap["daniel"]。

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

    我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 ``number去索引StringArray时会得到string类型的返回值。

    TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

    class Animal {
        name: string;
    }
    class Dog extends Animal {
        breed: string;
    }
    
    // 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
    interface NotOkay {
        [x: number]: Animal;
        [x: string]: Dog;
    }
    

    正确的使用

    interface NotOkay {
      [x: number]: Dog;
      [x: string]: Animal;
    }
    
    interface NumberDictionary {
      [index: string]: number;
      length: number;    // 可以,length是number类型
      name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
    }
    

    # 继承接口

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

    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;
    

    # 混合类型

    有时你会希望一个对象可以同时具有上面提到的多种类型。

    一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

    interface Counter {
        (start: number): string;
        interval: number;
        reset(): void;
    }
    
    function getCounter(): Counter {
        let counter = <Counter>function (start: number) { };
        counter.interval = 123;
        counter.reset = function () { };
        return counter;
    }
    
    let c = getCounter();
    c(10);
    c.reset();
    c.interval = 5.0;
    

    # 类 Class

    ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。

    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            return "Hello, " + this.greeting;
        }
    }
    
    let greeter = new Greeter("world");
    

    声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法。

    引用任何一个类成员的时候都用了 this, 它表示我们访问的是类的成员。

    # 继承

    class Animal {
        move(distanceInMeters: number = 0) {
            console.log(`Animal moved ${distanceInMeters}m.`);
        }
    }
    
    class Dog extends Animal {
        bark() {
            console.log('Woof! Woof!');
        }
    }
    
    const dog = new Dog();
    dog.bark();
    dog.move(10);
    

    类从基类中继承了属性和方法。 这里 Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。

    class Animal {
        name: string;
        constructor(theName: string) { this.name = theName; }
        move(distanceInMeters: number = 0) {
            console.log(`${this.name} moved ${distanceInMeters}m.`);
        }
    }
    
    class Snake extends Animal {
        constructor(name: string) { super(name); }
        move(distanceInMeters = 5) {
            console.log("Slithering...");
            super.move(distanceInMeters);
        }
    }
    
    class Horse extends Animal {
        constructor(name: string) { super(name); }
        move(distanceInMeters = 45) {
            console.log("Galloping...");
            super.move(distanceInMeters);
        }
    }
    
    let sam = new Snake("Sammy the Python");
    let tom: Animal = new Horse("Tommy the Palomino");
    
    sam.move();
    tom.move(34);
    

    创建了 Animal的两个子类: Horse和 Snake,派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

    这个例子演示了如何在子类里可以重写父类的方法。 Snake类和 Horse类都创建了 move方法,它们重写了从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。

    # public

    typeScript里,成员都默认为 public, 可以自由的访问程序里定义的成员。

    也可以明确的将一个成员标记成 public

    class Animal {
        public name: string;
        public constructor(theName: string) { this.name = theName; }
        public move(distanceInMeters: number) {
            console.log(`${this.name} moved ${distanceInMeters}m.`);
        }
    }
    

    # private

    当成员被标记成 private时,它就不能在声明它的类的外部访问 。

    class Animal {
        private name: string;
        constructor(theName: string) { this.name = theName; }
    }
    
    new Animal("Cat").name; // 错误: '属性“name”为私有属性,只能在类“Animal”中访问.
    

    # protected

    protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。

    class Person {
        protected name: string;
        constructor(name: string) { this.name = name; }
    }
    
    class Employee extends Person {
        private department: string;
    
        constructor(name: string, department: string) {
            super(name)
            this.department = department;
        }
    
        public getElevatorPitch() {
            return `Hello, my name is ${this.name} and I work in ${this.department}.`;
        }
    }
    let howard = new Employee("Howard", "Sales");
    console.log(howard.getElevatorPitch());
    console.log(howard.name); // 错误
    

    我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为 Employee是由 Person派生而来的。

    构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。

    class Person {
        protected name: string;
        protected constructor(theName: string) { this.name = theName; }
    }
    
    // Employee 能够继承 Person
    class Employee extends Person {
        private department: string;
    
        constructor(name: string, department: string) {
            super(name);
            this.department = department;
        }
    
        public getElevatorPitch() {
            return `Hello, my name is ${this.name} and I work in ${this.department}.`;
        }
    }
    
    let howard = new Employee("Howard", "Sales");
    let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.
    

    # readonly

    readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

    class Octopus {
        readonly name: string;
        readonly numberOfLegs: number = 8;
        constructor (theName: string) {
            this.name = theName;
        }
    }
    let dad = new Octopus("Man with the 8 strong legs");
    dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
    

    # 参数属性

    class Octopus {
      // readonly name: string
        readonly numberOfLegs: number = 8;
        constructor(readonly name: string) {
        }
    }
    

    在构造函数里使用 readonly name: string参数来创建和初始化 name成员。 我们把声明和赋值合并至一处。

    # 存取器

    TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

    let passcode = "secret passcode";
    
    class Employee {
        private _fullName: string;
    
        get fullName(): string {
            return this._fullName;
        }
    
        set fullName(newName: string) {
            if (passcode && passcode == "secret passcode") {
                this._fullName = newName;
            }
            else {
                console.log("Error: Unauthorized update of employee!");
            }
        }
    }
    
    let employee = new Employee();
    employee.fullName = "Bob Smith";
    if (employee.fullName) {
        alert(employee.fullName);
    }
    

    存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。

    # 静态属性

    class Grid {
        static origin = {x: 0, y: 0};
        calculateDistanceFromOrigin(point: {x: number; y: number;}) {
            let xDist = (point.x - Grid.origin.x);
            let yDist = (point.y - Grid.origin.y);
            return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
        }
        constructor (public scale: number) { }
    }
    
    let grid1 = new Grid(1.0);  // 1x scale
    let grid2 = new Grid(5.0);  // 5x scale
    
    console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
    console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
    

    我们使用 static定义 origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性。

    # 函数

    function add(x: number, y: number): number {
        return x + y;
    }
    
    let myAdd = function(x: number, y: number): number { return x + y; };
    

    每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

    let myAdd: (x: number, y: number) => number =
        function(x: number, y: number): number { return x + y; };
    

    返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void而不能留空。

    # 可选参数

    JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。

    function buildName(firstName: string, lastName?: string) {
        if (lastName)
            return firstName + " " + lastName;
        else
            return firstName;
    }
    
    let result1 = buildName("Bob");  // works correctly now
    let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
    let result3 = buildName("Bob", "Adams");  // ah, just right
    

    可选参数必须跟在必须参数后面。

    # 默认参数

    在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。

    function buildName(firstName: string, lastName = "Smith") {
        return firstName + " " + lastName;
    }
    
    let result1 = buildName("Bob");                  // works correctly now, returns "Bob Smith"
    let result2 = buildName("Bob", undefined);       // still works, also returns "Bob Smith"
    let result3 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
    let result4 = buildName("Bob", "Adams");         // ah, just right
    

    # 剩余参数

    想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数。

    在TypeScript里,你可以把所有参数收集到一个变量里

    function buildName(firstName: string, ...restOfName: string[]) {
      return firstName + " " + restOfName.join(" ");
    }
    
    let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
    

    剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。

    # 泛型

    可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。

    # 类型变量

    function identity<T>(arg: T): T {
        return arg;
    }
    

    我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

    适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

    可以用两种方法使用。

    • 第一种是,传入所有的参数,包含类型参数
    let output = identity<string>("myString");  // type of output will be 'string'
    

    明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来

    • 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型
    let output = identity("myString");  // type of output will be 'string'
    

    注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。

    # 使用泛型变量

    之前的例子, 如果我们想打印arg的长度

    function loggingIdentity<T>(arg: T): T {
        console.log(arg.length);  // Error: T doesn't have .length
        return arg;
    }
    

    编译器会报错说我们使用了arg的.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

    function loggingIdentity<T>(arg: T[]): T[] {
        console.log(arg.length);  // Array has a .length, so no more error
        return arg;
    }
    

    泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。

    也可以这样实现上面的例子:

    function loggingIdentity<T>(arg: Array<T>): Array<T> {
        console.log(arg.length);  // Array has a .length, so no more error
        return arg;
    }
    

    # 泛型类型

    function identity<T>(arg: T): T {
        return arg;
    }
    
    let myIdentity: <T>(arg: T) => T = identity;
    

    使用带有调用签名的对象字面量来定义泛型函数:

    function identity<T>(arg: T): T {
        return arg;
    }
    
    let myIdentity: {<T>(arg: T): T} = identity;
    

    把上面例子里的对象字面量拿出来做为一个接口:

    interface GenericIdentityFn {
        <T>(arg: T): T;
    }
    
    function identity<T>(arg: T): T {
        return arg;
    }
    
    let myIdentity: GenericIdentityFn = identity;
    
    interface GenericIdentityFn<T> {
        (arg: T): T;
    }
    
    function identity<T>(arg: T): T {
        return arg;
    }
    
    let myIdentity: GenericIdentityFn<number> = identity;
    

    # 泛型类

    class GenericNumber<T> {
        zeroValue: T;
        add: (x: T, y: T) => T;
    }
    
    let myGenericNumber = new GenericNumber<number>();
    myGenericNumber.zeroValue = 0;
    myGenericNumber.add = function(x, y) { return x + y; };
    

    一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型。

    泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

    GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

    let stringNumeric = new GenericNumber<string>();
    stringNumeric.zeroValue = "";
    stringNumeric.add = function(x, y) { return x + y; };
    
    console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
    

    # 泛型约束

    有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

    function loggingIdentity<T>(arg: T): T {
        console.log(arg.length);  // Error: T doesn't have .length
        return arg;
    }
    

    相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

    我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

    interface Lengthwise {
        length: number;
    }
    
    function loggingIdentity<T extends Lengthwise>(arg: T): T {
        console.log(arg.length);  // Now we know it has a .length property, so no more error
        return arg;
    }
    

    我们需要传入符合约束类型的值,必须包含必须的属性。

    loggingIdentity({length: 10, value: 3});
    

    # 类型推论

    在有些没有明确指出类型的地方,类型推论会帮助提供类型。

    let x = 3;
    

    变量x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

    # 最佳通用类型

    let x = [0, 1, null];
    

    为了推断x的类型,我们必须考虑所有元素的类型。 这里有两种选择: number和null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

    let zoo = [new Rhino(), new Elephant(), new Snake()];
    

    我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:

    let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
    

    如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]。

    # 上下文类型

    TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:

    window.onmousedown = function(mouseEvent) {
        console.log(mouseEvent.button);  //<- Error
    };
    

    TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent参数的类型了。 如果函数表达式不是在上下文类型的位置, mouseEvent参数的类型需要指定为any,这样也不会报错了。

    window.onmousedown = function(mouseEvent: any) {
        console.log(mouseEvent.button);  //<- Now, no error is given
    };
    

    上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:

    function createZoo(): Animal[] {
        return [new Rhino(), new Elephant(), new Snake()];
    }
    

    最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然, Animal会被做为最佳通用类型。

    # 类型兼容性

    TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。

    interface Named {
        name: string;
    }
    
    class Person {
        name: string;
    }
    
    let p: Named;
    // OK, because of structural typing
    p = new Person();
    

    TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性:

    interface Named {
        name: string;
    }
    
    let x: Named;
    // y's inferred type is { name: string; location: string; }
    let y = { name: 'Alice', location: 'Seattle' };
    x = y;
    

    这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y包含名字是name的string类型成员,满足条件,因此赋值正确。

    检查函数参数时使用相同的规则:

    function greet(n: Named) {
        console.log('Hello, ' + n.name);
    }
    greet(y); // OK
    

    y有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容。

    # 比较两个函数

    如何判断两个函数是兼容的:

    let x = (a: number) => 0;
    let y = (b: number, s: string) => 0;
    
    y = x; // OK
    x = y; // Error
    

    要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

    第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

    如何处理返回值类型,创建两个仅是返回值类型不同的函数:

    let x = () => ({name: 'Alice'});
    let y = () => ({name: 'Alice', location: 'Seattle'});
    
    x = y; // OK
    y = x; // Error, because x() lacks a location property
    

    类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

    # 枚举

    枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。

    enum Status { Ready, Waiting };
    enum Color { Red, Blue, Green };
    
    let status = Status.Ready;
    status = Color.Green;  // Error
    

    # 类

    类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

    class Animal {
        feet: number;
        constructor(name: string, numFeet: number) { }
    }
    
    class Size {
        feet: number;
        constructor(numFeet: number) { }
    }
    
    let a: Animal;
    let s: Size;
    
    a = s;  // OK
    s = a;  // OK
    

    类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。

    # 泛型

    TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。

    interface Empty<T> {
    }
    let x: Empty<number>;
    let y: Empty<string>;
    
    x = y;  // OK, because y matches structure of x
    

    上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。

    interface NotEmpty<T> {
        data: T;
    }
    let x: NotEmpty<number>;
    let y: NotEmpty<string>;
    
    x = y;  // Error, because x and y are not compatible
    

    对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。

    let identity = function<T>(x: T): T {
        // ...
    }
    
    let reverse = function<U>(y: U): U {
        // ...
    }
    
    identity = reverse;  // OK, because (x: any) => any matches (y: any) => any