TypeScript程式語言的介面,是TypeScript特有、JavaScript沒有的東西,它與declare關鍵字類似,只是用來寫給編譯器看的。TypeScript的介面可以將物件型別抽像化並加上名稱,形成新的型別,還能讓類別根據介面來添加屬性。



還記得在上一章中,我們是怎麼撰寫物件的型別嗎?

用以下這個大括號{}物件為例:

const o = {
    name: "David",
    age: 18,
    toString: function () {
        return `${this.age}: ${this.name}`;
    },
};

它的型別就是{ name: string, age: number, toString: () => string }。所以我們可以用以下的方式來明確定義變數o的型別:

const o: { name: string, age: number, toString: () => string } = {
    name: "David",
    age: 18,
    toString: function () {
        return `${this.age}: ${this.name}`;
    },
};

我們可以透過TypeScript的介面,將{ name: string, age: number, toString: () => string }這個型別加上名稱。例如要將其命名為User,程式如下:

interface User {
    name: string,
    age: number,
    toString: () => string
}

const o: User = {
    name: "David",
    age: 18,
    toString: function () {
        return `${this.age}: ${this.name}`;
    },
};

如何?有沒有覺得程式更容易閱讀了?

用類別物件實作介面

我們還可以在撰寫類別物件的時候,於類別名稱後面使用implements關鍵字來指定要實作的介面。介面擁有的屬性,除了可選的屬性之外,實作該介面的類別也都要有。

例如:

interface User {
    name: string,
    age: number,
    toString: () => string
}

class Student implements User {
    name: string;

    age: number;

    toString: () => string;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.toString = () => `${this.age}: ${this.name}`;
    }
}

我們無法用instanceof關鍵字來判斷某個值是否屬於指定的介面。例如以下程式會編譯錯誤:

interface User {
    name: string,
    age: number,
    toString: () => string
}

class Student implements User {
    name: string;

    age: number;

    toString: () => string;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.toString = () => `${this.age}: ${this.name}`;
    }
}

const o = new Student("David", 18);

console.log(o instanceof User);

implements關鍵字和extends關鍵字可以一同使用,但是implements關鍵字必須要放在extends關鍵字之後。

例如:

class User {
    name: string;

    protected type = "user";

    constructor(name: string) {
        this.name = name;
    }

    whoAmI() {
        return `My name is ${this.name}. I am a ${this.type}.`;
    }
}

class Human extends User {
    constructor(name: string) {
        super(name);
        this.type = "human";
    }
}

class Robot extends User {
    constructor(name: string) {
        super(name);
        this.type = "robot";
    }
}

interface Studiable {
    study: () => void
}

class Student extends Human implements Studiable {
    study = () => {
        console.log("I'm studying!");
    };

    constructor(name: string) {
        super(name);
        this.type = "student";
    }
}

extends關鍵字只能繼承一個類別。implements關鍵字則可以用來實作多個介面,用逗號,隔開多個介面名稱。

例如:

class User {
    name: string;

    protected type = "user";

    constructor(name: string) {
        this.name = name;
    }

    whoAmI() {
        return `My name is ${this.name}. I am a ${this.type}.`;
    }
}

class Human extends User {
    constructor(name: string) {
        super(name);
        this.type = "human";
    }
}

class Robot extends User {
    constructor(name: string) {
        super(name);
        this.type = "robot";
    }
}

interface Studiable {
    study: () => void
}

interface Expellable {
    expel: () => void
}

class Student extends Human implements Studiable, Expellable {
    study = () => {
        console.log("I'm studying!");
    };

    expel = () => {
        console.log("Noooo!");
    };

    constructor(name: string) {
        super(name);
        this.type = "student";
    }
}

擴充介面

藉由重複撰寫interface關鍵字,可以替已存在的介面擴充原本不存在的屬性。

例如:

interface Point {
    x: number,
    y: number
}

interface Point {
    z: number
}

class Vertex implements Point {
    x: number;

    y: number;

    z: number;

    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

以上程式,Point介面實際為:

interface Point {
    x: number,
    y: number,
    z: string
}

介面繼承介面

介面也可以使用extends關鍵字來繼承其它介面。這功能其實就是剛才提到的「擴充介面」,只不過多用了extends關鍵字來保留原本的介面名稱。

例如:

interface Point2D {
    x: number,
    y: number
}

interface Point3D extends Point2D {
    z: number
}

class Vertex implements Point3D {
    x: number;

    y: number;

    z: number;

    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

與類別不同的是,類別的extends關鍵字後只能接上一個類別名稱,而介面的extends關鍵字後可以接上多個介面名稱或是類別名稱,要用逗號,隔開。

例如:

interface PointX {
    x: number
}

interface PointY {
    y: number
}

interface PointZ {
    z: number
}

interface Point2D extends PointX, PointY {

}

interface Point3D extends Point2D, PointZ {

}

class Vertex implements Point3D {
    x: number;

    y: number;

    z: number;

    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

介面和類別的名稱重複

在TypeScript中,介面和類別的名稱是可以重複的。重複名稱的介面會擴充類別的屬性,但類別可以不必照著這個重複名稱的介面來實作程式。

例如:

interface Student {
    dropOut: () => void
}

class Student {
    name: string;

    age: number;

    toString: () => string;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.toString = () => `${this.age}: ${this.name}`;
    }
}

const o = new Student("David", 18);

o.dropOut();

以上程式雖然可以成功編譯,但會執行失敗,因為Student物件實體並沒有dropOut方法。

抽象類別(Abstract Class)

在定義一個類別的時候,可以在其class關鍵字前加上abstract,使其變成抽象類別。TypeScript的抽象類別和一般類別的差異不大,只不過就是不能夠被「new」而已。

例如:

interface PointX {
    x: number
}

interface PointY {
    y: number
}

interface PointZ {
    z: number
}

interface Point2D extends PointX, PointY {

}

interface Point3D extends Point2D, PointZ {

}

abstract class AbstractVertex implements Point3D {
    x: number;

    y: number;

    z: number;

    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

class Vertex extends AbstractVertex {
    constructor(public x: number, public y: number, public z: number) {
        super(x, y, z);
    }
}

以上的AbstractVertex類別是一個抽象類別,它不能夠被用new關鍵字來產生物件實體,但是它可以被繼承,也可以透過其子類別Vertex的建構子來呼叫到AbstractVertex類別的建構子。

我們也可以在抽象類別的屬性(欄位或是方法都可以)前加上abstract關鍵字來修飾,使得繼承這個抽象類別的類別必須要實作這個屬性。使用abstract關鍵字來修飾的屬性,不需指定值,也不需實作出函數主體,只要把名稱和型別定義出來就好了。

例如:

abstract class Animal {
    abstract readonly name: string;

    abstract makeSound(): void;
  
    move(): void {
        console.log("roaming the earth...");
    }
}

class Dog extends Animal {
    readonly name = "Dog";

    makeSound(): void {
        console.log("Woof!");
    }
}

總結

在這個章節中我們學會了TypeScript提供的介面與抽象類別的用法,在下一個章節要來學習TypeScript提供的列舉用法。

下一章:TypeScript程式語言的列舉