玩转Typescript(三):TypeScript 接口

1,553 阅读7分钟

这是我参与11月更文挑战的第 3 天,活动详情查看:2021最后一次更文挑战

前言

刚从Javascript转Typescript时,看到interface(接口)会感到疑惑。接口的写法类似于函数声明的写法function xxx() {},使用了关键字interface。然而这有什么用呢?

interface Person {
  name?: string,
  age: number,
  sayHi: () => string
}
interface SetUser {
 (name: string, age: number): void;
}

结合TypeScript为Javascript增加了类型检查的特点,我们可以看到interface(接口)为变量定义了一个“模板”,是一系列属性和抽象方法的声明,满足了TypeScript的静态类型检查的需求。

我们可以在接口中定义属性和方法,然后在定义变量(描述一个对象或者函数)时使用到。

const man: Person = {
  name: "Tom",
  age: 18,
  sayHi: () => "Hi"
}

此外,在面向对象编程的过程中,借助接口可以做到继承(extends)实现(implements)

类似的关键字有type

type User = {
  name?: string
  age: number
};
type SetUser = (name: string, age: number): void;

接下来介绍interface(接口)的具体用法。

可选属性

interface SquareConfig {
  color?: string;
  width?: number;
}

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。

interface Point {
  readonly x: number;
  readonly y: number;
}
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!

把整个ReadonlyArray赋值到一个普通数组也是不可以的,但是你可以用类型断言重写:

a = ro; // error!
a = ro as number[];

readonly vs const

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

额外属性检查

对象字面量会被特殊对待而且会经过额外属性检查。如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}
let mySquare = createSquare({ colour: "red", width: 100 }); // error: 'colour' not expected in type 'SquareConfig'

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

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

最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。

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

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

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

函数类型

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

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}
let a = mySearch('abc', 'bc')
console.log(a) // true

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,对应位置上的参数类型兼容即可。如上所示,source -> srcsubString -> sub

可索引的类型

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

interface StringArray {
  [index: number]: string;
}
let myArray: StringArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
console.log(myStr);  // "Bob"

这个索引签名表示了当用 number去索引StringArray时会得到string类型的返回值。

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

下面的例子里,name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

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

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

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error! 你不能设置myArray[2],因为索引签名是只读的。

类类型

类实现接口

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}
class Clock implements ClockInterface {
  currentTime: Date;
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {
    this.currentTime = new Date();
    // 不初始化会提示:`Property 'currentTime' has no initializer and is not definitely assigned in the constructor.(2564)`
  }
}

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

去除初始化提示的方法:tsconfig.json > compilerOptions, 增加一项:"strictPropertyInitialization": false,这样就不用对属性进行初始化了。

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

实例:

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

当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时,会得到一个错误 Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any',这里因为类是具有两个类型的:静态部分的类型和实例的类型。当一个类实现了一个接口时,只对其实例部分进行类型检查。constructor存在于类的静态部分,所以不在检查的范围内。因此,我们应该直接操作类的静态部分。

下面的例子中,我们定义了两个接口,ClockConstructor为构造函数所用,ClockInterface为实例方法所用。 为了方便我们定义函数createClock,它用传入的类型创建实例。

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick(): void;
}
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");
  }
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute);
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
digital.tick();
analog.tick();

因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

继承接口

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

interface Shape {
  color: string;
}
interface Square extends Shape {
  sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

注释:尖括号内Square是泛型,<Square>{}断言类型(Type Assertions)。另一种写法:let square = {} as Square;

一个接口可以继承多个接口,创建出多个接口的合成接口。

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;

混合类型

接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

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

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

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。

接口同样会继承到类的private和protected成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

class Control {
  private state: any;
}
interface SelectableControl extends Control {
  select(): void;
}
class Button extends Control implements SelectableControl { // Button是Control的子类
  select() { }
}
// 错误:“Image”类型缺少“state”属性
class Image implements SelectableControl { // Image不是Control的子类
  select() { }
}

SelectableControl包含了Control的所有成员,包括私有成员state。 因为 state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。

参考