TypeScript进阶

作者: wave浪儿 | 来源:发表于2018-10-11 16:39 被阅读10次

    1.类型别名

    类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

    type Name = string;
    type NameResolver = () => string;
    type NameOrResolver = Name | NameResolver;
    function getName(n: NameOrResolver): Name {
        if (typeof n === 'string') {
            return n;
        }
        else {
            return n();
        }
    }
    

    起别名不会新建一个类型 ,它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

    同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

    type Container<T> = { value: T };
    

    我们也可以使用类型别名来在属性里引用自己:

    type Tree<T> = {
        value: T;
        left: Tree<T>;
        right: Tree<T>;
    }
    

    与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

    type LinkedList<T> = T & { next: LinkedList<T> };
    
    interface Person {
        name: string;
    }
    
    var people: LinkedList<Person>;
    var s = people.name;
    var s = people.next.name;
    var s = people.next.next.name;
    var s = people.next.next.next.name;
    

    我们使用 type 创建类型别名。类型别名常用于联合类型
    类型别名不能出现在声明右侧的任何地方。

    接口 vs. 类型别名

    像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
    其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

    type Alias = { num: number }
    interface Interface {
        num: number;
    }
    declare function aliased(arg: Alias): Alias;
    declare function interfaced(arg: Interface): Interface;
    

    另一个重要区别是类型别名不能被 extendsimplements(自己也不能 extendsimplements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
    另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

    2.字符串字面量类型

    字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

    type Easing = "ease-in" | "ease-out" | "ease-in-out";
    class UIElement {
        animate(dx: number, dy: number, easing: Easing) {
            if (easing === "ease-in") {
                // ...
            }
            else if (easing === "ease-out") {
            }
            else if (easing === "ease-in-out") {
            }
            else {
                // error! should not pass null or undefined.
            }
        }
    }
    
    let button = new UIElement();
    button.animate(0, 0, "ease-in");
    button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
    

    你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

    3.元组 Tuple

    数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
    元组起源于函数编程语言(如 F#),在这些语言中频繁使用元组。
    定义一对值分别为 stringnumber的元组:

    let xcatliu: [string, number] = ['Xcat Liu', 25];
    

    当赋值或访问一个已知索引的元素时,会得到正确的类型:

    let xcatliu: [string, number];
    xcatliu[0] = 'Xcat Liu';
    xcatliu[1] = 25;
    
    xcatliu[0].slice(1);
    xcatliu[1].toFixed(2);
    

    也可以只赋值其中一项:

    let xcatliu: [string, number];
    xcatliu[0] = 'Xcat Liu';
    

    但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

    let xcatliu: [string, number];
    xcatliu = ['Xcat Liu', 25];
    
    //下面两种都会报错
    let xcatliu: [string, number] = ['Xcat Liu'];
    //这样也会报错
    let xcatliu: [string, number];
    xcatliu = ['Xcat Liu'];
    xcatliu[1] = 25;
    

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

    let xcatliu: [string, number];
    xcatliu = ['Xcat Liu', 25];
    //这个不会报错
    xcatliu.push('http://xcatliu.com/');
    //这个会报错
    xcatliu.push(true);
    

    4.枚举

    使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。枚举类型中是包含双向映射的,即(value -> name)和(name -> value)
    数字枚举

    enum Direction {
        Up = 1,
        Down,
        Left,
        Right
    }
    console.info(Direction);
    //双向映射
    console.info(Direction.Down);//获取枚举的值
    console.info(Direction[2]); //获取枚举值对应的名称定义
    
    

    如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。
    我们还可以完全不使用初始化器:

    enum Direction {
        Up,
        Down,
        Left,
        Right,
    }
    

    使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:

    enum Response {
        No = 0,
        Yes = 1,
    }
    
    function respond(recipient: string, message: Response): void {
        // ...
    }
    
    respond("Princess Caroline", Response.Yes)
    

    字符串枚举
    在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

    enum Direction {
        Up = "UP",
        Down = "DOWN",
        Left = "LEFT",
        Right = "RIGHT",
    }
    

    常数项和计算所得项
    枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

    //不会报错
    enum Color {Red, Green, Blue = "blue".length};
    //会报错
    enum Color {Red = "red".length, Green, Blue};
    

    上面的例子中,"blue".length 就是一个计算所得项。
    如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错。

    当满足以下条件时,枚举成员被当作是常数:

    1. 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
    2. 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
      • 数字字面量
      • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中
        定义的,可以使用非限定名来引用
      • 带括号的常数枚举表达式
      • +, - ,~ 一元运算符应用于常数枚举表达式
      • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式作为其一个操作对象。若
        常数枚举表达式求值后为NaN或Infinity,则会在编译阶段报错。

    所有其它情况的枚举成员被当作是需要计算得出的值。

    常数枚举
    当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。 常数枚举是在enum关键字前使用const修饰符。
    常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员

    const enum Direction {
        //常数
        Up, //0
        Down = 3, //3
        Left = Down + 4, //7
        Right, //8
    
        //计算的值
        center = [1,2,3,4].length //error
    }
    let directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
    

    编译结果:

    var directions = [0 /* Up */, 3 /* Down */, 7 /* Left */, 8 /* Right */];
    

    外部枚举
    外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

    declare enum Directions {
        Up,
        Down,
        Left,
        Right
    }
    
    let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
    

    之前提到过,declare 定义的类型只会用于编译时的检查,编译结果中会被删除。
    上例的编译结果是:

    var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
    

    外部枚举与声明语句一样,常出现在声明文件中。
    同时使用 declare 和 const 也是可以的:

    declare const enum Directions {
        Up,
        Down,
        Left,
        Right
    }
    
    let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
    

    编译结果:

    var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
    

    5.Symbols

    Symbol也是值,但它不是字符串,也不是对象,而是是全新的——第七种类型的原始值。
    从一个简单的布尔类型出发:
    举个例子,假设你正在写一个JS库,可以通过CSS transitions使DOM元素在屏幕上移动。你可能会注意到,当你尝试在一个div元素上同时应用多重CSS transitions时并不会生效。实际效果是丑陋而又不连续的“跳闪”。你认为可以修复这个问题,但前提是你需要一种发现给定元素是否已经移动过的方 法。
    应当如何解决这个问题呢?
    一种方法是,用CSS API来告诉浏览器元素是否正在移动,但这样简直小题大做。在元素移动的第一时间内你的库就应该记录下移动的状态,所以它自然知道元素正在移动。
    你真正想要的是一种持续跟踪某个元素正在移动的方法。你可以维护一个数组,记录所有正在移动的元素,每当你的库被调用来移动某个元素时,你可以检索数组来查看元素是否已经存在,亦即它是否正在移动中。
    当然,如果数组非常大的话,线性搜索将会非常缓慢。
    实际上你只想为元素设置一个标记:

        if (element.isMoving) {
          smoothAnimations(element);
        }
        element.isMoving = true;
    

    这样也会有一些潜在的问题,事实上,你的代码很可能不是唯一一段操作DOM的代码。

    • 你创建的属性很可能影响到其它使用了for-in或Object.keys()的代码。
    • 一些聪明的库作者可能已经考虑并使用了这项技术,这样一来你的库就会与已有的库产生某些冲突
    • 当然,很可能你比他们更聪明,你先采用了这项技术,但是他们的库仍然无法与你的库默契配合。
    • 标准委员会可能决定为所有的元素增加一个.isMoving()方法,到那时你需要重写相关逻辑,必定会有深深的挫败感。
      当然你可以选择一个乏味而愚蠢的命名(其他人根本不会想用的那些名称)来解决最后的三个问题:
        if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
          smoothAnimations(element);
        }
        element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
    

    这只会造成无畏的眼疲劳。
    借助于密码学,你可以生成一个唯一的属性名称:

        // 获取1024个Unicode字符的无意义命名
        var isMoving = SecureRandom.generateName();
        ...
        if (element[isMoving]) {
          smoothAnimations(element);
        }
        element[isMoving] = true;
    

    object[name]语法允许你使用几乎任何字符串作为属性名称。所以这个方法行之有效:冲突几乎是不可能的,并且你的代码看起来也很简洁。

    但是这也将带来不良的调试体验。每当你在控制台输出(console.log())包含那个属性的元素时,你将会看到一堆巨大的字符串垃圾。假使你需要比这多得多的类似属性呢?你如何保持它们整齐划一?每当你重载的时候它们的命名甚至都不一样!

    为什么这个问题如此困难?我们只想要一个小小的布尔值啊!

    symbol是最终的解决方案

    symbol是程序创建并且可以用作属性键的值,并且它能避免命名冲突的风险。

        var mySymbol = Symbol();
    

    调用Symbol()创建一个新的symbol,它的值与其它任何值皆不相等。

    字符串或数字可以作为属性的键,symbol也可以,它不等同于任何字符串,因而这个以symbol为键的属性可以保证不与任何其它属性产生冲突。

        obj[mySymbol] = "ok!";  // 保证不会冲突
        console.log(obj[mySymbol]);  // ok!
    

    想要在上述讨论的场景中使用symbol,你可以这样做:

        // 创建一个独一无二的symbol
        var isMoving = Symbol("isMoving");//使用Symbol来创建,其中引号内的内容被称作描述
        ...
        if (element[isMoving]) {//如果有以Symbol为键isMoving为描述的属性
          smoothAnimations(element);
        }
        element[isMoving] = true;
    

    有关这段代码的一些解释:

    • Symbol("isMoving")中的isMoving被称作描述。你可以通过console.log()将它打印出来,对调试非常有帮助;你也可以用.toString()方法将它转换为字符串呈现;它也可以被用在错误信息中。
    • element[isMoving]被称作一个以symbol为键(symbol-keyed)的属性。简而言之,它的名字是symbol而不是一个字符串。除此之外,它与一个普通的属性没有什么区别。
    • 以symbol为键的属性属性与数组元素类似,不能被类似obj.name的点号法访问,你必须使用方括号访问这些属性。
    • 如果你已经得到了symbol,那么访问一个以symbol为键的属性同样简单,以上的示例很好地展示了如何获取element[isMoving]的值以及如何为它赋值。如果我们需要,可以查看属性是否存在:if (isMoving in element),也可以删除属性:delete element[isMoving]
    • 另一方面,只有当isMoving在当前作用域中时才会生效。这是symbol的弱封装机制:模块创建了几个symbol,可以在任意对象上使用,无须担心与其它代码创建的属性产生冲突。

    到底什么是symbol?

        > typeof Symbol()
        "symbol"
    

    symbol被创建后就不可变更,你不能为它设置属性
    每一个symbol都独一无二,不与其它symbol等同,即使二者有相同的描述也不相等
    symbol不能被自动转换为字符串,这和语言中的其它类型不同
    尝试拼接symbol与字符串将得到TypeError错误:

        > var sym = Symbol("<3");
        > "your symbol is " + sym
        // TypeError: can't convert symbol to string
        > `your symbol is ${sym}`
        // TypeError: can't convert symbol to string
    

    通过String(sym)sym.toString()可以显示地将symbol转换为一个字符串,从而回避这个问题。
    获取symbol的三种方法:

    • 调用Symbol()。正如我们上文中所讨论的,这种方式每次调用都会返回一个新的唯一symbol。
    • 调用Symbol.for(string)。这种方式会访问symbol注册表,其中存储了已经存在的一系列symbol。这种方式与通过Symbol()定义的独立symbol不同,symbol注册表中的symbol是共享的。如果你连续三十次调用Symbol.for("cat"),每次都会返回相同的symbol。注册表非常有用,在多个web页面或同一个web页面的多个模块中经常需要共享一个symbol。
    • 使用标准定义的symbol,例如:Symbol.iterator。标准根据一些特殊用途定义了少许的几个symbol。

    6.类

    传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class

    类的概念

    这里对类相关的概念做一个简单的介绍:

    • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
    • 对象(Object):类的实例,通过 new生成
    • 面向对象(OOP)的三大特性:封装、继承、多态
    • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
    • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
    • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 CatDog都继承自 Animal,但是分别实现了自己的 eat方法。此时针对某一个实例,我们无需了解它是 Cat还是 Dog,就可以直接调用 eat方法,程序会自动判断出来应该如何执行 eat
    • 存取器(getter & setter):用以改变属性的读取和赋值行为
    • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
    • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
    • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口

    ES6 中类的用法

    下面我们先回顾一下 ES6 中类的用法,更详细的介绍可以参考 ECMAScript 6 入门 - Class

    属性和方法

    使用 class定义类,使用 constructor定义构造函数。
    通过 new 生成新实例的时候,会自动调用构造函数。

    class Animal {
        constructor(name) {
            this.name = name;
        }
        sayHi() {
            return `My name is ${this.name}`;
        }
    }
    let a = new Animal('Jack');
    console.log(a.sayHi()); // My name is Jack
    
    类的继承

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

    class Cat extends Animal {
        constructor(name) {
            super(name); // 调用父类的 constructor(name)
            console.log(this.name);
        }
        sayHi() {
            return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
        }
    }
    
    存取器

    使用 getter 和 setter 可以改变属性的赋值和读取行为:

    class Animal {
        constructor(name) {
            this.name = name;
        }
        get name() {
            return 'Jack';
        }
        set name(value) {
            console.log('setter: ' + value);
        }
    }
    
    let a = new Animal('Kitty'); // setter: Kitty
    a.name = 'Tom'; // setter: Tom
    console.log(a.name); // Jack
    
    静态方法

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

    class Animal {
        static isAnimal(a) {
            return a instanceof Animal;
        }
    }
    
    let a = new Animal('Jack');
    Animal.isAnimal(a); // true
    a.isAnimal(a); // TypeError: a.isAnimal is not a function
    

    ES7 中类的用法

    ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。

    实例属性

    ES6 中实例的属性只能通过构造函数中的 this.xxx来定义,ES7 提案中可以直接在类里面定义:

    class Animal {
        name = 'Jack';
    
        constructor() {
            // ...
        }
    }
    
    let a = new Animal();
    console.log(a.name); // Jack
    
    静态属性

    ES7 提案中,可以使用 static 定义一个静态属性:

    class Animal {
        static num = 42;
    
        constructor() {
            // ...
        }
    }
    
    console.log(Animal.num); // 42
    

    TypeScript 中类的用法

    public private 和 protected

    TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

    • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
    • private修饰的属性或方法是私有的,不能在声明它的类的外部访问
    • protected修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

    下面举一些例子:

    class Animal {
        public name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    let a = new Animal('Jack');
    console.log(a.name); // Jack
    a.name = 'Tom';
    console.log(a.name); // Tom
    

    上面的例子中,name被设置为了 public,所以直接访问实例的 name属性是允许的。
    很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private了:

    class Animal {
        private name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    let a = new Animal('Jack');
    console.log(a.name); // error
    a.name = 'Tom'; // error
    

    需要注意的是,TypeScript 编译之后的代码中,并没有限制 private属性在外部的可访问性。
    上面的例子编译后的代码是:

    var Animal = (function () {
        function Animal(name) {
            this.name = name;
        }
        return Animal;
    }());
    var a = new Animal('Jack');
    console.log(a.name);
    a.name = 'Tom';
    

    使用 private修饰的属性或方法,在子类中也是不允许访问的:

    class Animal {
        private name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    class Cat extends Animal {
        constructor(name) {
            super(name);
            console.log(this.name); //error
        }
    }
    

    而如果是用 protected修饰,则允许在子类中访问:

    class Animal {
        protected name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    class Cat extends Animal {
        constructor(name) {
            super(name);
            console.log(this.name);
        }
    }
    
    抽象类

    abstract 用于定义抽象类和其中的抽象方法。
    什么是抽象类?
    首先,抽象类是不允许被实例化的:

    abstract class Animal {
        public name;
        public constructor(name) {
            this.name = name;
        }
        public abstract sayHi();
    }
    
    let a = new Animal('Jack'); //error
    

    上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。
    其次,抽象类中的抽象方法必须被子类实现:

    abstract class Animal {
        public name;
        public constructor(name) {
            this.name = name;
        }
        public abstract sayHi();
    }
    //error
    class Cat extends Animal {  
        public eat() {
            console.log(`${this.name} is eating.`);
        }
    }
    
    let cat = new Cat('Tom');
    

    上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。
    下面是一个正确使用抽象类的例子:

    abstract class Animal {
        public name;
        public constructor(name) {
            this.name = name;
        }
        public abstract sayHi();
    }
    
    class Cat extends Animal {
        public sayHi() {
            console.log(`Meow, My name is ${this.name}`);
        }
    }
    
    let cat = new Cat('Tom');
    

    上面的例子中,我们实现了抽象方法 sayHi,编译通过了。
    需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:

    var __extends = (this && this.__extends) || function (d, b) {
        for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
    var Animal = (function () {
        function Animal(name) {
            this.name = name;
        }
        return Animal;
    }());
    var Cat = (function (_super) {
        __extends(Cat, _super);
        function Cat() {
            _super.apply(this, arguments);
        }
        Cat.prototype.sayHi = function () {
            console.log('Meow, My name is ' + this.name);
        };
        return Cat;
    }(Animal));
    var cat = new Cat('Tom');
    
    类的类型

    给类加上 TypeScript 的类型很简单,与接口类似:

    class Animal {
        name: string;
        constructor(name: string) {
            this.name = name;
        }
        sayHi(): string {
          return `My name is ${this.name}`;
        }
    }
    
    let a: Animal = new Animal('Jack');
    console.log(a.sayHi()); // My name is Jack
    

    7.类与接口

    之前学习过,接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。
    这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。

    类实现接口

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

    举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

    interface Alarm {
        alert();
    }
    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();
    }
    
    interface Light {
        lightOn();
        lightOff();
    }
    class Car implements Alarm, Light {
        alert() {
            console.log('Car alert');
        }
        lightOn() {
            console.log('Car light on');
        }
        lightOff() {
            console.log('Car light off');
        }
    }
    

    上例中,Car实现了 AlarmLight接口,既能报警,也能开关车灯。

    接口继承接口

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

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

    上例中,我们使用 extends 使 LightableAlarm继承 Alarm

    接口继承类

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

    混合类型

    可以使用接口的方式来定义一个函数需要符合的形状:

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

    有时候,一个函数还可以有自己的属性和方法:

    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;
    

    8.泛型

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

    简单的例子

    首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

    //只能返回string类型的数据
    function getData(value:string):string{
        return value;
    }
    //同时返回 string类型 和number类型  (代码冗余)
    function getData1(value:string):string{
       return value;
     }
    function getData2(value:number):number{
        return value;
     }
    //同时返回 string类型 和number类型       any可以解决这个问题
     function getData(value:any):any{
        return '哈哈哈';
    }
    getData(123);
    getData('str');
    
    //传入的参数类型和返回的参数类型可以不一致
    function getData(value:any):any{
           return '哈哈哈';
     }
    

    any放弃了类型检查,它并没有准确的定义返回值的类型,如果想要传入什么 返回什么。比如:传入number 类型必须返回number类型 传入 string类型必须返回string类型,这时候,泛型就派上用场了。
    泛型:可以支持不特定的数据类型
    要求:传入的参数和返回的参数一致

    //T表示泛型,具体什么类型是调用这个方法的时候决定的:
    function getData<T>(value:T):T{
           return value;
    }
    getData<number>(123);
    getData<string>('1214231');
    getData<number>('2112');       /*错误的写法*/  
    

    泛型接口

    可以使用接口的方式来定义一个函数需要符合的形状:

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

    当然也可以使用含有泛型的接口来定义函数的形状:

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

    进一步,我们可以把泛型参数提前到接口名上:

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

    注意,此时在使用泛型接口的时候,需要定义泛型的类型。

    泛型类

    与泛型接口类似,泛型也可以用于类的类型定义中:

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

    泛型参数的默认类型

    在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

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

    相关文章

      网友评论

        本文标题:TypeScript进阶

        本文链接:https://www.haomeiwen.com/subject/qrudaftx.html