Skip to content

接口初探

接口可以被实现、被继承,type 不能。 type 可以写联合类型

typescript
interface IPerson {
  name: string,
  age: number
}
// 给接口定义一个调用签名,参数列表里的每个参数都需要名字和类型。
interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

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

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性。 可选属性的该属性允许不存在,但不允许添加未定义的属性。

typescript
// 1.可以对可能存在的属性进行预定义。
// 2.可以捕获引用了不存在的属性时的错误。 
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" }); // Ok
let mySquare = createSquare({ colors: "black" }); // Error

只读属性

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

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

// 只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
let p1: Point = { x: 10, y: 20 }; // 给只读属性赋值,赋值后, x和y再也不能被改变了。
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[];

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

额外的属性检查

对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候

typescript
interface Foo {
  bar: string,
  baz: string
}

const o: Foo = { bar: 'bar', baz: 'baz', name: 'Tom' } // Error

// 绕开这些检查非常简单。 最简便的方法是使用类型断言:
const o: Foo = { bar: 'bar', baz: 'baz', name: 'Tom' } as Foo // Ok

interface Foo {
  bar: string,
  baz: string,
  [propName: string]: any // 带有任意数量的其它属性
}

// 同名接口会合并

// 
interface NewFoo extends interface Foo {
  [propName; string]: any // 带有任意数量的其它属性 
}

// 还有一种跳过这些检查的方式,就是将这个对象赋值给一个另一个变量:
// 因为 o1 不会经过额外属性检查,所以编译器不会报错。
interface Foo {
  bar: string,
  baz: string
}

const o1 = { bar: 'bar', baz: 'baz', name: 'Tom' }
const o2: Foo = o1

任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

typescript
interface Person {
    name: string;
    age?: number;
    [propName: string]: any; // 定义了任意属性取 string 类型的值。
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

// 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
interface Person {
    name: string;
    age?: number; // number 不是 string 类型的子类型
    [propName: string]: string;
}

// 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

接口定义函数

typescript
interface FullName {
  (source: string, subString: string): string;
}

const fullName: FullName = function(firstName, lastName) {
  return firstName + '-' + lastName
}

可索引的类型

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

typescript
interface StringArray {
  // 表示当用 number 去索引 StringArray 时会得到 string 类型的返回值。
  [index: number]: string; // index 是自定义的名字
}

let myArray: StringArray = ["Bob", "Fred"]
let myStr: string = myArray[0]

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

// 字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配
// 因为字符串索引声明了 obj.property和obj["property"]两种形式都可以。
// 下面的例子里, 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!

类类型

typescript
// TypeScript也能够用它来明确的强制一个类去符合某种契约。
// 接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

interface FooInterface {
  bar: string,
  baz (): void // 描述类的原型方法时,void 表示不关心该方法的返回值。
}

class Foo implements FooInterface {
  name!: string

  speak (): string {
    return ''
  }
} 

interface OtherFooInterface {
  // ...
}

class Foo implements FooInterface, OtherFooInterface {}


interface ClockInterface {
  currentTime: Date;
}

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

// 你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:
interface ClockInterface {
  currentTime: Date;
  setTime(d: Date);
}

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

// 当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。
// 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor {
  new (hour: number, minute: number);
}

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

// 这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。
// constructor存在于类的静态部分,所以不在检查的范围内。

// 因此,我们应该直接操作类的静态部分。
// 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。
// 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。

interface ClockConstructor {
  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);

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

// 抽象类不能被实例化,可以包含抽象属性和抽象方法。
abstract class Foo {
  abstract name: string // 可以没有实现

  // 可以包含非抽象的方法
  eat () {

  }
}

class Bar extends Foo {
  name!: string // 子类必须实现父类的抽象属性和抽象方法
}

接口表示实例

ts
class Foo {
  name: string
  constructor (name) {
    this.name = name
  }
}

interface IClass { // 表示一个构造函数类型
  new(name: string): Foo // 可以将类当成类型,但是这样就写死了。使用泛型解决 
}

const createInstance = (class: IClass, name: string) => {
  return new Foo()
}

const createInstance = (class: {new (name: string): any}, name: string) => {
  return new Foo()
}

const createInstance = (class: new (name: string) => any, name: string) => {
  return new Foo()
}

继承接口

typescript
// 和类一样,接口也可以相互继承。
// 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
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;

混合类型

typescript
// 一个对象可以同时做为函数和对象使用,并带有额外的属性。
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;
// 在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。

接口继承类

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

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类并不是这样的。