TypeScript 自学笔记3 接口

前言

  • 个人学习笔记,仅供参考

介绍

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

接口初探

  • 类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配
  • 不但要求传入参数还要求这个参数里面有一个名为label的string参数
  • 如果没有把label这个必须传入的参数传入就会报错
    1
    2
    3
    4
    5
    6
    function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
    }

    let myObj = { size: 10, label: "Size 10 Object" };
    printLabel(myObj);

  • 重写这个例子
  • 类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以
  • interface 泛指接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 接口     后面这个是名称
    // 定义一个名为LabelledValue的接口
    interface LabelledValue {
    label: string;
    }

    // 在接收参数时,作为指定类型引用
    function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
    }

    let myObj = {size: 10, label: "Size 10 Object"};
    printLabel(myObj);

可选属性

  • 这个可选属性我们在之前也有学过了
  • 带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号
  • 接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

  • 下面是应用了“option bags”的例子:
  • 这里考到了两个知识点
      1. 可选属性的使用
      1. 返回类型的指定
  • 重点:
    • 可选参数
    • 返回参数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 接口 
      interface SquareConfig {
      // 两个可选属性
      color?: string;
      width?: number;
      }

      // 函数参数调用接口,可不传参数,给空对象
      // 但是返回中,指定了必须返回 color 和area这两个属性
      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)

  • 在Ts中更好的体现了,权限的控制
  • 限制为只读属性
  • 一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 1. 定义一个接口,x变量设置为只读
    interface Point {
    readonly x: number;
    readonly y: number;
    }

    // 2. 构造出一个point的对象
    let p1: Point = {x:10,y:20}

    // 3.尝试改变内部的x值,会说这是个只读属性不能修改
    p1.x = 5;// err Cannot assign to 'x' because it is a read-only property.

ReadonlyArray 只读数组

  • 不要以为只有数据有,数组也是有的哦
  • ReadonlyArray 类型用于数组
  • 一旦创建后无法更改,不能赋值,也不能赋值给别的数据
    1
    2
    3
    4
    5
    6
    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赋值到一个普通数组也是不可以的
  • 但是有一种情况可以赋值给别人那就是使用类型断言重写
  • 这样就是可以赋值的
    1
    let b = ro as number[];

readonly vs const

  • 最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

额外的属性检查

额外检查错误

  • 我们学会了可选属性知道了optionbages 模式的使用
  • 但是把可选属性和传值结合在一起时会发出错误,就是在参数中加入指定类型以外的属性
  • ts中定义了类型指定后,会做额外的检查
  • 虽然是可选属性但是还是报出错误,限制没有属性就会报错
  • TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    interface SquareConfig {
    color?: string;
    width?: number;
    }

    function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
    }

    // 注意传入的参数colour
    // error: 'colour' not expected in type 'SquareConfig'
    // 在指定类型中是没有的
    let mySquare = createSquare({ colour: "red", width: 100 });

解决额外检查错误(绕开检查)

解决方法一 (断言)

  • 在传入参数时后面加入断言
    1
    let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

解决方法二 (完美解法,添加一个字符串索引签名)

  • 前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性
  • 如果 SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它
  • 所以虽然是最完美的解决方法,但是要一开始就确立是否会带有
  • 只要它们不是color和width,那么就无所谓它们的类型是什么。
    1
    2
    3
    4
    5
    6
    interface SquareConfig {
    color?: string;
    width?: number;
    // 字符串索引签名
    [propName: string]: any;
    }

解决方法三 (耍赖皮,没有理解,我觉得这样是一个漏洞)

  • 将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错
    1
    2
    let squareOptions = { colour: "red", width: 100 };
    let mySquare = createSquare(squareOptions);

  • 要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入 color或colour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

函数类型

  • 接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型
  • 为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
  • 其实和声明函数差不多,只是没有了前面的function和后面的业务
  • 使用像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 定义名为 SearchFunc 的接口
    interface SearchFunc {
    // 定义参数为source,subString 都为string
    // 返回类型为boolean
    (source: String, subString: String) : boolean;
    }

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

    console.log(mySearch('aaabb','bb'))

  • 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配
  • 因为只是参数,传入时也无法判断
    1
    2
    3
    4
    5
    let mySearch: SearchFunc;
    mySearch = function(src: string, sub: string): boolean {
    let result = src.search(sub);
    return result > -1;
    }

  • 函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配
  • 定义时可以不写指定类型
  • 但是传入和返回类型不匹配就会报错
    1
    2
    3
    4
    5
    let mySearch: SearchFunc;
    mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
    }

可索引的类型

数字索引(数组)

  • 第一感觉这个索引是为数组而设立的
  • 通过不同类型的值去索引
  • 与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或ageMap[“daniel”]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
  • 我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 number去索引StringArray时会得到string类型的返回值
    1
    2
    3
    4
    5
    6
    7
    8
    interface StringArray {
    [index: number]: string;
    }

    let myArray: StringArray;
    myArray = ["Bob", "Fred"];

    let myStr: string = myArray[0];

字符串索引(对象)

  • TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 错误
    class Animal {
    name: string;
    }
    class Dog extends Animal {
    breed: string;
    }

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

  • 字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property和obj[“property”]两种形式都可以
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface NumberDictionary {
    [index: string]: number;
    length: number; // 可以,length是number类型
    name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
    }

    let myArray: NumberDictionary;
    myArray = {test1:100,length:20,test2:200};

    console.log(myArray.test1)
    console.log(myArray.test2)
    console.log(myArray.length)

防止索引被篡改 (只读)

  • 最后,你可以将索引签名设置为只读,这样就防止了给索引赋值
    1
    2
    3
    4
    5
    interface ReadonlyStringArray {
    readonly [index: number]: string;
    }
    let myArray: ReadonlyStringArray = ["Alice", "Bob"];
    myArray[2] = "Mallory"; // error!

类类型

实现接口

  • 与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
  • 其实就是java的抽象方法
  • 定义了就必须去执行
  • 接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

  1. 元素
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 实现元素
    interface ClockInterface {
    currentTime: Date;
    }

    class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
    }

  1. 方法
  • 你也可以在接口中描述一个方法,在类里实现它
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
    // constructor(d: Date) // 这个是不允许的构造函数是不允许这样被定义的
    }

    class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
    this.currentTime = d;
    return this.currentTime;
    }
    constructor(h: number, m: number) { }
    }

    let test = new Clock(10,20);
    console.log(test.setTime(new Date())); //2019-03-09T14:12:10.617Z

类静态部分与实例部分的区别 (令人费解的地方,未知)

  • 这个地方不太好理解
  • 当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误
  • 因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内
  • 因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名

  1. 这个直指类中的构造函数,就是在new时要传参,并有返回值
  2. 因为在类中是不会去检索构造函数,所以直接定义就会出错
  3. 所以我们在fn中传参数时定义,就会去检测构造函数签名
  4. 其实就是说作为参数和类是不想同的检索方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 错误
    interface ClockConstructor {
    new (hour: number, minute: number);
    }

    class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
    }

    ------------------------------
    // 正确
    interface ClockConstructor {
    // 约束 new 一个实例,直接针对class的构造函数
    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);
    console.log(digital.tick())

继承接口

  • 和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里
  • 这个就比较好理解,就是儿子继承爸爸的东西
  • 也可多接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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;

混合类型

  • 一个对象可以同时做为函数和对象使用,并带有额外的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

接口继承类

  • 当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)
  • 当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Control {
    private state: any;
    }

    interface SelectableControl extends Control {
    select(): void;
    }

    class Button extends Control implements SelectableControl {
    select() { }
    }

    class TextBox extends Control {
    select() { }
    }

    // 错误:“Image”类型缺少“state”属性。
    class Image implements SelectableControl {
    select() { }
    }

    class Location {

    }
  • 在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为 state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的

  • 在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上, SelectableControl接口和拥有select方法的Control类是一样的。 Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的

后记

  • 这个就是我学习Ts的第三天的笔记,欢迎更多的同行大哥指导交流
  • 欢迎进入我的博客https://yhf7.github.io/
  • 如果有什么侵权的话,请及时添加小编微信以及qq也可以来告诉小编(905477376微信qq通用),谢谢!
-------------本文结束感谢您的阅读-------------
0%