接口interface

104 阅读5分钟

接口interface

介绍

接口(Interfaces)是一种强大的特性,它允许你定义对象的形状,即对象应该具有哪些属性以及这些属性的类型。 在TypeScript里,主要用于类型检查和提供代码的自动完成功能,它们不直接实现任何功能,而是作为类型约束的蓝图。

基本用法

一个接口定义了一个对象的结构,而不实现它。然后,你可以创建一个实现该接口的对象。

interface Person {  
    name: string;  
    age: number;  
    greet: (phrase: string) => void;  
}  
  
// 实现Person接口的对象  
let person: Person = {  
    name: "Alice",  
    age: 30,  
    greet: function(phrase: string) {  
        console.log(`${phrase}, my name is ${this.name}.`);  
    }  
};  
  
person.greet("Hello");

可选属性

接口中的属性可以标记为可选的,这意味着这些属性在对象中可以不存在。

interface Person {  
    name: string;  
    age?: number; // 可选属性  
}  
  
let person1: Person = {  
    name: "Bob"  
};  
  
let person2: Person = {  
    name: "Carol",  
    age: 25  
};

只读属性

接口中的属性也可以被标记为只读,这意味着一旦在对象上设置了这些属性的值,之后就不能再修改它们。

interface Point {  
    readonly x: number;  
    readonly y: number;  
}  
  
let p1: Point = { x: 10, y: 20 };  
// p1.x = 5; // 错误: 'x' 是只读的。

readonly vs const

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

函数类型

在接口中,你也可以定义函数类型,就像上面的greet方法一样。

索引签名

索引签名允许你描述对象索引的类型。当你有一个对象,但是你不确定对象上会有哪些属性,但你知道这些属性的类型时,索引签名就非常有用。

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。

interface StringDictionary {  
    [index: string]: string;  
}  
// 应用场景:一个复杂对象,我只能确定某些属性的时候,其他的无法确定
let myDict: StringDictionary = {  
    "firstName": "John",  
    "lastName": "Doe",  
    "age": "30" // 严格来说,这不是一个好主意,因为'age'应该是number类型  
};

类类型

接口也可以描述类的形状,当类实现了接口时,类的实例就可以被赋值给接口类型的变量。

interface ClockInterface {  
    currentTime: Date;  
    setTime(d: Date);  
}  
  
class Clock implements ClockInterface {  
    currentTime: Date;  
  
    constructor(h: number, m: number) {  
        this.currentTime = new Date();  
        this.currentTime.setHours(h, m, 0, 0);  
    }  
  
    setTime(d: Date) {  
        this.currentTime = d;  
    }  
}  
  
let clock = new Clock(7, 30);  
let c: ClockInterface = clock;

接口是TypeScript中非常重要的概念,它们帮助开发者定义和约束代码的结构,从而写出更加健壮和易于维护的代码。

继承接口

接口(Interface)之间不能直接像类(Class)那样使用extends关键字进行继承。但是,接口可以“继承”另一个或多个接口,这实际上是一种组合接口的方式,使得你可以在一个接口中复用另一个接口的成员(属性或方法)。

当你说一个接口“继承”另一个接口时,你实际上是在创建一个新的接口,这个新接口包含了被“继承”接口的所有成员,并且你还可以添加新的成员。

示例

假设我们有两个接口,Animal 和 CanFly。Animal 接口表示一个基本的动物属性,而 CanFly接口表示一个可以飞行的能力。现在,我们想要创建一个新的接口 Bird,它既是 Animal 也是 CanFly。

在TypeScript中,我们可以这样做:

// 定义Animal接口  
interface Animal {  
    name: string;  
    eat(): void;  
}  
  
// 定义CanFly接口  
interface CanFly {  
    fly(): void;  
}  
  
// Bird接口继承Animal和CanFly接口  
interface Bird extends Animal, CanFly {  
    // 这里可以添加Bird特有的属性或方法,或者重定义继承来的属性/方法(如果需要的话)  
    // 例如,我们可以为Bird的fly方法添加一些额外的说明或限制  
    fly(height: number): void; // 注意:这实际上是重写了CanFly接口中的fly方法,可能需要额外的逻辑来确保兼容性  
    // 但通常,我们不会这样做,除非有特定的理由。在这个例子中,我们保持fly方法不变。  
}  
  
// 创建一个符合Bird接口的对象  
let bird: Bird = {  
    name: "Eagle",  
    eat: () => console.log(`${bird.name} is eating.`),  
    fly: () => console.log(`${bird.name} is flying.`), // 注意:这里我们并没有实现fly(height: number),因为那样会违反Bird接口的定义  
    // 如果我们要实现fly(height: number),我们应该确保在fly方法中处理height参数  
};  
  
// 注意:上面的bird对象实际上不符合Bird接口,因为它没有实现fly(height: number)。  
// 为了修正这一点,我们需要修改fly方法的实现:  
let correctedBird: Bird = {  
    name: "Eagle",  
    eat: () => console.log(`${correctedBird.name} is eating.`),  
    fly: (height: number) => console.log(`${correctedBird.name} is flying at ${height} meters.`)  
};

重要提示:在上面的示例中,我展示了如何“重写”继承自CanFly接口的fly方法,但请注意,这实际上是在Bird接口中定义了一个新的fly方法,该方法具有不同的签名(即它接受一个height参数)。在TypeScript中,这并不意味着你“覆盖”了CanFly接口中的fly方法;相反,你是在Bird接口中定义了一个新的fly方法,该方法与CanFly接口中的fly方法共存(但在这个上下文中,由于Bird同时继承了Animal和CanFly,并且Bird中的fly方法具有更具体的签名,因此在Bird类型的上下文中,只会看到Bird接口中定义的fly方法)。

然而,在大多数情况下,你不会在接口中“重写”继承来的方法,而是会添加新的属性或方法,或者确保你的实现对象符合所有继承接口的要求。

混合类型

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

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

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第三方库的时候,你可能需要像上面那样去完整地定义类型。

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:

class Control {
    private state: any;
}
// 接口I继承A类的目的,就是类C要实现这个接口I,C必须要是A的子类
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类并不是这样的。

在TypeScript中,接口(Interface)不能直接继承类(Class) ,因为接口和类的设计目的和用途是不同的。接口定义了对象的形状(即对象应该有哪些属性和方法),而类则提供了这些属性和方法的具体实现。

然而,如果你想要一个类型既包含类的一些行为(方法),又可能包含额外的属性或方法,你可以考虑以下几种方案:

1. 使用抽象类

如果你的目的是创建一个可以被其他类继承的“基类”,并且这个基类包含一些未实现的方法(即抽象方法),那么你应该使用抽象类而不是接口。

abstract class Animal {  
    name: string;  
  
    constructor(name: string) {  
        this.name = name;  
    }  
  
    abstract makeSound(): void; // 抽象方法,必须由子类实现  
}  
  
class Dog extends Animal {  
    makeSound() {  
        console.log('Woof!');  
    }  
}

2. 接口与类的组合

如果你想要一个类型同时包含类的实例属性和方法,以及接口定义的额外属性或方法,你可以让类实现接口,并在类中提供接口所需的所有属性和方法。

interface Speakable {  
    speak(): void;  
}  
  
class Animal {  
    name: string;  
  
    constructor(name: string) {  
        this.name = name;  
    }  
}  
  
class Dog extends Animal implements Speakable {  
    speak() {  
        console.log(`${this.name} says Woof!`);  
    }  
}

在这个例子中,Dog 类继承了 Animal 类的属性,并实现了 Speakable 接口的方法。

3. 使用类型别名和交叉类型

如果你想要一个类型同时包含某个类的实例类型和其他类型(可能是另一个接口)的混合,你可以使用类型别名和交叉类型。但是,请注意,这并不会创建一个继承自类的接口,而是创建了一个新的类型,该类型结合了类的实例类型和其他类型的特性。

interface Speakable {  
    speak(): void;  
}  
  
class Animal {  
    name: string;  
  
    constructor(name: string) {  
        this.name = name;  
    }  
}  
  
type AnimalWithSpeak = Animal & Speakable;  
  
// 注意:你不能直接实例化AnimalWithSpeak,因为它是一个交叉类型。  
// 但你可以通过类型断言或创建一个实现了Speakable接口的Animal子类来“模拟”它。  
  
let dog: AnimalWithSpeak = {  
    // 这里需要确保对象同时满足Animal和Speakable的要求  
    // 但由于JavaScript对象字面量不能直接实现接口,所以通常需要类型断言或类继承  
    // 这里只是演示概念,实际上会报错  
    // name: "Rex",  
    // speak: () => console.log("Woof!"),  
    // ...(需要类型断言或继承)  
} as AnimalWithSpeak; // 类型断言,但通常不推荐这样做,因为它绕过了类型检查  
  
// 更常见的做法是创建一个类来同时满足这些要求  
class DogWithSpeak extends Animal implements Speakable {  
    speak() {  
        console.log(`${this.name} says Woof!`);  
    }  
}  
  
let actualDog: AnimalWithSpeak = new DogWithSpeak("Rex"); // 正确的方式

总结

在TypeScript中,接口不能直接继承类。如果你需要类似的行为,你应该考虑使用抽象类、让类实现接口,或者使用类型别名和交叉类型来组合多个类型。但是,请记住,每种方法都有其适用场景和限制。