Typescript - Interfaces (接口)

192 阅读7分钟

TypeScript 中, 接口 interface 用来描述一系列属性或者方法以及它们的类型声明的集合结构,它在内部和外部代码中很好地提供一种结构数据的类型约束。

第一个例子

function printLabel(labeledObj: { label: string }) {
  console.log(labeledObj.label);
}

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

类型检查会判断传递给函数的参数是否是一个包含 label 属性并且类型为 string 的对象。值得注意的是,传递的对象参数可以包含其他属性。

可以用 interface 改造上面例子:

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

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

可以看出函数参数必须实现这个接口,另外 Typescript 不会去校验属性的顺序,只要包含接口的属性以及正确类型即可。

可选属性

当接口描述的属性可选时,可以用 ?: 表示:

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

如果访问了不在接口描述的属性,将会抛出错误。 所以对于可能出现的属性,都应该在接口中描述出来。

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) {
// Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
// Property 'clor' does not exist on type 'SquareConfig'. Did you mean '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;
}

可以对象字面量构造 Point, 创建后, xy 就不能被修改了:

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
//  Cannot assign to 'x' because it is a read-only property.

TypeScript 提供了一个 ReadonlyArray<T> 数组类型,不同于 Array<T> 的是,它移除了所有数组变异方法,这样就能保证创建数组后就不能进行任何修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // error!
// Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
// Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
// Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
// The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

从上面的例子最后一行可以看到把 ro 重新辅助回去也会报错,可以用类型断言解决:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

a = ro as number[];

readonly vs const: 在声明变量的时候使用 const, 在声明属性的时候用 readonly

额外属性检查

在上面第一个例子中,Typescript 允许我们传递一个具有额外属性的对象给函数,但是把对象换成对象字面量方式传递的话情况就不一样了:

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  return { color: config.color || "red", area: config.width ? config.width*config.width : 20 };
}

let mySquare = createSquare({ colour: "red", width: 100 });
// Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
//   Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

用对象字面量赋值给一个接口类型变量或者作为函数参数会出发 TypeScript 的额外属性检测(Excess Proerty Checks)。如果存在没有接口声明中的属性,就会抛出异常。同样的,我们可以用类型断言解决这个报错:

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

更好的一种解决方案是为接口增加动态参数声明,如下:

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

我们稍后再详细介绍接口动态属性,目前只要知道我们传递的字面量对象可以包含任意额外的任意类型的属性。另外有一个很奇怪的解决方法就是可以把字面量赋值给一个对象,再把对象作为函数参数,也不会报错:

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

上面不报错的一个原因是传递的对象和接口声明具有公共的属性 width,否则也会抛出异常:

let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
// Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.

声明函数

接口也可以用来声明函数类型,在接口内声明函数参数列表及类型和返回类型即可:

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

像其他接口一样使用函数类型的接口:

let mySearch: SearchFunc;

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

函数参数名可以不用和声明时一样,并且可以省略参数的类型声明,它会根据接口声明时的类型进行检查,返回类型根据 return 类型进行检查:

let mySearch: SearchFunc;

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

返回类型和声明时不一样就会抛出异常:

let mySearch: SearchFunc;

mySearch = function (src, sub) {
  let result = src.search(sub);
   // Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
  // Type 'string' is not assignable to type 'boolean'.
  return "string";
};

动态属性声明

接口允许我们声明动态属性来解决 obj["key"]a[10] 动态赋值的场景,动态属性通过 [key: type]: type 格式声明:

interface StringArray {
  [index: number]: string;
}

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

let myStr: string = myArray[0];

索引的类型只支持 stringnumber 类型。在接口中可以同时支持这两种类型,但是数子索引的值类型必须时字符串索引的值类型的子类型,因为在数字索引在对象中会被先转为字符串,比如 a[10] 会转为 a['10']

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
  [x: number]: Animal;
// Numeric index type 'Animal' is not assignable to string index type 'Dog'.
  [x: string]: Dog;
}

如果使用 string 类型的索引,它会检查接口的所有属性值必须为索引返回的类型。否则会报错:

interface NumberDictionary {
  [index: string]: number;
  length: number; // ok, length is a number
  name: string; // error, the type of 'name' is not a subtype of the indexer
// Property 'name' of type 'string' is not assignable to string index type 'number'.
}

动态索引如果返回一个联合类型的话可以解决上面的报错:

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

最后,如果给动态属性加上只读 readonly 限制可以防止动态分配额外属性:

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
// Index signature in type 'ReadonlyStringArray' only permits reading.

Class Types

接口的实现

和其他静态语言一样,可以定义一个类来实现接口:

interface ClockInterface {
  currentTime: Date;
}

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

对于接口定义的方法,也要有具体的实现方法:

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

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

注意的是类实现接口的属性和方法是为 public 的不是 private

类的静态和实例在实现接口中的不同

如果我们定义一个类型实现一个具有构造标示(new 函数声明)的接口时,会报错:

interface ClockConstructor {
  new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
// Class 'Clock' incorrectly implements interface 'ClockConstructor'.
  // Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
  currentTime: Date;
  constructor(h: number, m: number) {}
}

这是因为 Typescript 只会校验类的实例是否实现了接口,不会去校验类的静态属性。

接口继承

和类一样,接口之间可以通过继承来扩展属性,从而使接口定义更加灵活和可复用:

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

另外接口还支持多重继承:

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as 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 = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;