Interface 是 TypeScript 的招牌菜

TypeScript 與 ECMAScript 最大的差別就是 interface,一些 TypeScript 先進的功能,陸續被 ECMAScript 所接受,但由於 ECMAScript 偏向動態弱型別觀念,將來採用 interface 的機會渺茫,interface 也成為 TypeScript 與 ECMAScript 最大的分水嶺。

Version


TypeScript 2.5

TypeScript Interface


有別於一般強型別語言的 interface,TypeScript 一共提供 6 種 interface

  • Object interface
  • Index interface
  • Class interface
  • Constructor interface
  • Function interface
  • Hybrid interface

Object Interface


定義 object 的 public property

1
2
3
4
5
6
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}

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

printLabel() 期望傳入的 paremeter 為 { label: string } 型別的 object,只有一個 label property。

但實際上傳入的 myObj,除了有 label property 外,還多了 size property,但也通過了 TypeScript 的編譯檢查。

也就是說,TypeScript 採用的是 duck typing 策略 :

只要傳入的 argument 有 parameter 要求的 property 即可,因此 argument 可以傳入比 parameter 要求更多的 property 的物件。

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}

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

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

printLabel() 的 parameter 型別,除了直接寫在 function 的參數列外,若這個型別檢查會被重複使用,則建議抽成 LabelledValue interface。

目前還沒看到有任何重構工具可以將 parameter 的型別抽成 interface,只能手動作。

Q : 什麼是 Duck Typing 與 Strong Typing ?

Duck Typing

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。

白話 : 物件只要有該型別相同的 property 與 method,就算是該 class 型別。

用於動態弱型別 script。

執行階段檢查型別是否正確。

JavaScript 、Ruby 屬於 Duck Typing。

Strong Typing

由母鴨生產的鴨子,才算是鴨子。

白話 : 物件必須透過 class 的 new 建立,物件才算是該 class 型別。

用於強型別編譯語言。

編譯階段檢查型別是否正確。

C++、Java、C# 屬於 Strong Typing。

Q : TypeScript 算 strong typing 還是 duck typing ?

TypeScript 因為要相容 JavaScript,且最後也是編譯成 JavaScript,所以 TypeScript 本質是 duck typing,卻在編譯階段檢查型別是否正確,算是融合 strong typing 與 duck typing。

Duck typing 的靈活

  • 不須 new 則可使用

Strong typing 的嚴謹

  • interface 描述規格比較好維護
  • IDE 的 intellisense
  • 編譯階段檢查

目前程式語言的強型別與弱型別已經漸漸模糊,如 C# 也開始吸取弱型別語言的優點 : vardynamic,PHP 也開始學習強型別語言的精華 : type hint 與 interface。

Optional Property

實務上有些 property 有預設值,因此在傳入 function 時可以不用提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

const mySquare1 = createSquare({color:'black'});
console.log(mySquare1.area); // 100

const mySquare2 = createSquare({color:'black', width:20});
console.log(mySquare2.area); // 400

const mySquare3 = createSquare({});
console.log(mySquare3.area); // 100

第 1 行

1
2
3
4
interface SquareConfig {
color?: string;
width?: number;
}

colorwidthSquareConfig 的 property,但因為加上 ?,因此也可省略。

17 行

1
2
const mySquare1 = createSquare({color:'black'});
console.log(mySquare1.area); // 100

因此可以直提供一個 color property 即可。

20 行

1
2
const mySquare2 = createSquare({color:'black', width:20});
console.log(mySquare2.area); // 400

提供完整的兩個 colorwidth property 的 object 當然沒問題。

23 行

1
2
const mySquare3 = createSquare({});
console.log(mySquare3.area); // 100

提供一個 empty object,完全沒有 property 亦可。

第 6 行

1
2
3
4
5
6
7
8
9
10
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;
}

也因為 property 為 optional,所以必須檢查是否有 property,並提供預設值。

Excess Property Checkes

1
2
3
4
5
6
7
8
9
interface LabelledValue {
label: string;
}

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

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

在之前我們知道 TypeScript 對於 object 使用 duck typing 檢查型別,但對於 object literal,TypeScript 則採用 strong typing 方式。

Object Literal

直接使用 {} 方式建立物件

labelledObj 型別只有 label property,傳入 {size: 10, label: "Size 10 Object"} 多了 size property,TypeScript 編譯時將產生以下錯誤 :

1
2
3
4
error TS23
45: Argument of type '{size: number; label: string; }' is not assign
able to parameter of type 'LabelledValue'.
Object literal may only specify known properties, and 'size' does not exist in type 'LabelledValue'

若以 duck typing 角度而言,label property 可視為多餘的 property,無傷大雅,但實務上,多出來的 property 很可能是 typo,如 API 已經規定了 JSON 物件 spec,你只會傳遞 spec 定好 property 的 JSON 物件,而不會多傳其他 property,多出來的 property 大概都是 typo。

TypeScript takes the stance that there’s probably a bug in this code. Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.

根據 TypeScript 官網文件,對於 Excess Property Checkes 做了以下的解釋 :

TypeScript 對於這類程式碼,採取可能有 bug 的態度。當 object literal 指定給其他變數,或者以 argument 方式傳入 function,將採取 excess property check 方式檢查型別,如果 object literal 有 target type 所沒有的 property,將編譯錯誤。

Q : 若真的要傳入比 interface 還多 property 的 object,我該怎麼做 ?

  1. 使用 Object 傳入
  2. 使用 Type Assertion
  3. 使用 String Index Signature

使用 Object 傳入

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}

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

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

由於 excess property checks 只針對 object literal,因此改傳 object。

先將 object literal 指定給 myObj,再將 myObj 傳入 printLabel

使用 Type Assertion

1
2
3
4
5
6
7
8
9
interface LabelledValue {
label: string;
}

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

printLabel(<LabelledValue>{size: 10, label: "Size 10 Object"});

先使用 type assertion 將 {size: 10, label: "Size 10 Object"} 轉型成 LabelledValue 型別,再傳入 printLabel()

使用 String Index Signature

1
2
3
4
5
6
7
8
9
10
11
interface LabelledValue {
label: string;
[propName:string]: any;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
console.log(labelledObj['size']);
}

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

以上兩種做法,基本上就是將多餘的 property 視而不見,目的只是為了避開編譯錯誤,若你想將多餘的 parameter 變成一種附加的資訊,可由 user 自行決定是否傳入,且 function 也可順利存取,則建議改變 interface,改用 string index signature 處理多餘的 property。

1
2
3
4
interface LabelledValue {
label: string;
[propName:string]: any;
}

在 interface 增加 [propName:string]: any,其中 property name 將成為 index name,由於不限制 user 傳入的 property 型別,因此為 any

1
printLabel({size: 10, label: "Size 10 Object"});

User 因此可以傳入額外的 size property,TypeScript 也不會報錯。

1
2
3
4
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
console.log(labelledObj['size']);
}

還可使用 labelledObj['size'] 存取其他 property。

Readonly Property

1
2
3
4
5
6
7
interface Point {
readonly x: number;
readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

當 object 只有在一開始建立時才能設定其 property,之後就不再能修改其 property 時,可在 property 名稱之前加上 readonly

Q : 該如何分辨 readonly 與 const ?

  • readonly : 用於 property
  • const : 用於 variable

Q : 實務上哪裡會使用 object interface ?

  1. 傳進 API 的 JSON 物件,或從 API 傳回的 JSON 物件,會宣告其 object interface。
  2. Component 與 service 之間的 method 以物件傳遞時 (DTO : Data Transfer Object),會宣告其 object interface。

Index Interface


定義物件的 index signature

JavaScript 的 object,也可以使用類似陣列的 [] 方式存取,[] 可為 number 或 string。

Index 為 Number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface CloudDictionary {
[index: number]: string;
}

let clouds: CloudDictionary = {};
clouds[0] = 'aws';
clouds[1] = 'azure';
clouds[2] = 'gcp';

console.log(clouds[0]);
console.log(clouds[1]);
console.log(clouds[2]);
// aws
// azure
// gcp

第 1 行

1
2
3
interface CloudDictionary {
[index: number]: string;
}

定義 [] 內的 index 為 number 型別,而 [] 的回傳值為 string。

第 5 行

1
2
3
4
let clouds: CloudDictionary = {};
clouds[0] = 'aws';
clouds[1] = 'azure';
clouds[2] = 'gcp';

宣告 clouds 物件為 CloudDictionary index interface 型別。

clouds 的 index 必須為 number,內容必須為 string,否則 TypeScript 會編譯錯誤。

Index 為 String

1
2
3
4
5
6
7
8
9
10
11
12
interface CloudDictionary {
[key: string]: number;
}

let clouds: CloudDictionary = {};
clouds['aws'] = 0;
clouds['azure'] = 1;
clouds['gcp'] = 2;

console.log(clouds['aws']);
console.log(clouds['azure']);
console.log(clouds['gcp']);

第 1 行

1
2
3
interface CloudDictionary {
[key: string]: number;
}

Index signature 也可以是 string。

第 5 行

1
2
3
4
let clouds: CloudDictionary = {};
clouds['aws'] = 0;
clouds['azure'] = 1;
clouds['gcp'] = 2;

宣告 clouds 物件為 CloudDictionary index interface 型別。

clouds 的 index 必須為 string,內容必須為 number,否則 TypeScript 會編譯錯誤。

Index 可為 Number 與 String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface CloudDictionary {
[index: number]: string;
[index: string]: number | string;
}

let clouds: CloudDictionary = {};
clouds[0] = 'aws';
clouds[1] = 'azure';
clouds[2] = 'gcp';
clouds['aws'] = 0;
clouds['azure'] = 1;
clouds['gcp'] = 2;


console.log(clouds[0]);
console.log(clouds[1]);
console.log(clouds[2]);
console.log(clouds['aws']);
console.log(clouds['azure']);
console.log(clouds['gcp']);
// aws
// azure
// gcp
// aws
// azure
// gcp

若希望將前面兩個範例合一,讓 CloudDictionary 同時可接受 number 與 string 的 index。

第 1 行

1
2
3
4
interface CloudDictionary {
[index: number]: string;
[index: string]: number | string;
}

比較 tricky 的是這一段 :

1
[index: string]: number | string;

除了原本的 number,還要加上 string。

因為 clouds[1],在 JavaScript 也可以寫成 clouds['1'],所以若要寫 string index signature 時,還必須考慮相容於 number index signature,否則 TypeScript 會編譯錯誤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
}

interface IZoo {
[x: number]: Dog;
[x: string]: Animal;
}

let zoo: IZoo = {};
zoo[0] = new Dog('Sam');
zoo['Kevin'] = new Animal('Kevin');

console.log(zoo[0].name);
console.log(zoo['Kevin'].name);
// Sam
// Kevin

第 7 行

1
2
3
4
interface IZoo {
[x: number]: Dog;
[x: string]: Animal;
}

若 index 的 return type 是 class type,理論上應該寫成 [x: string]: Animal | Dog,但因為 Dog 繼承於 Animal,基於里氏替換原則 : 父類別可以用子類別代替,簡化成 Animal 即可。

Index 搭配 Property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface CloudDictionary {
[index: string]: number;
name: number;
}

let clouds: CloudDictionary = {
name: 10
};
clouds['aws'] = 0;
clouds['azure'] = 1;
clouds['gcp'] = 2;

console.log(clouds['aws']);
console.log(clouds['azure']);
console.log(clouds['gcp']);
console.log(clouds.aws);
// 0
// 1
// 2
// 0

第 1 行

1
2
3
4
interface CloudDictionary {
[index: string]: number;
name: number;
}

Index interface 若包含 property,則 property 的型別必須與 index 的回傳型別相同。

因為在 JavaScript,obj["property"] 也可以寫成 obj.property,所以 property 的型別必須與 index 型別一致。

Class Interface


定義 class 的 public method 與 property

一般物件導向語言 (C#、Java、PHP) 的 interface 都屬於 class interface,要求 class 須具備哪些 public method 與 property 時,會使用 interface 特別定義。

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}

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

ClockInterface 定義了 class 該有 currentTime property,且型別為 Date

Clock class implementsClockInterface 後,就必須有 currentTime property,且型別為 Date,否則會編譯錯誤。

可在 class type interface 定義 public property。

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}

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

ClockInterface 定義了 class 該有的 setTime() method,其 signature 為 setTime(d: Date)

Clock class implementsClockInterface 後,就必須有 setTime() method,且 signature 為 setTime(d: Date),否則會編譯錯誤。

可在 class interface 定義 public method。

Interface 只能定義 class 的 public 部分,無法定義其 private 與 protected 部分。

Constructor Interface


定義 constructor 的 signature

Class interface 不會去定義 constructor 的 signature,但有時候自己寫 constructor function / factory method 建立物件時,基於依賴反轉原則,我們會希望 object 有我們期望的 signature,因此會定義出 constructor interface,要求 class 去實踐,且受 TypeScript 編譯器檢查。

1
2
3
4
5
6
7
8
interface ClockConstructor {
new (hour: number, minute: number);
}

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

第 1 行

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

因為 new 會呼叫 constructor,所以使用 new 代表 constructor 描述所期望的 signature。

第 5 行

1
2
3
4
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
} // error

基於 class interface 的經驗,我們會很直覺的使用 implements 配合 constructor interface,但 TypeScript 編譯後會得到以下錯誤 :

error TS2420: Class ‘Clock’ incorrectly implements interface ‘ClockConstructor’.
Type ‘Clock’ provides no match for the signature ‘new (hour: number, minute: number): any’.

錯誤訊息指出 Clock 找不到符合 new signature 的 method。

Q : 再談物件的 class level 與 object level

我們知道物件的 property 與 method 分 class level 與 object level,如 static 屬於 class level,一般非 static 屬於 object level,所以也稱為 static sideinstance side

Q : Constructor 該屬於 class level 還是 object level 呢 ?

Object level 的 property 與 method 要透過 new 之後才能使用,所以 new 不該屬於 object level,只能是 class level。

Clock class implementsClockConstructor interface,TypeScript 嘗試在 object level 去尋找符合 new signature 的 method,但 constructor(h: number, m: number) 屬於 class level,所以 TypeScript 在 object level 找不到符合 new signature 的 method,因而報錯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface ClockInterface {
tick();
}

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

class Clock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}

let clock = createClock(Clock, 12, 17);
clock.tick();

第 1 行

1
2
3
interface ClockInterface {
tick();
}

ClockInterface 為 class interface,定義 Clock class 該有哪些 object level 的 method。

第 5 行

1
2
3
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}

ClockConstructor 為 construtor interface,定義 Clock class 的 constructor 的 signature。

第 9 行

1
2
3
4
5
6
class Clock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}

Clock class implmentsClockInterface,此為 object level 的 interface,故可以使用 implements

Clock class 無法 implments ClockConstructor interface,因為此為 class level 的 interface。

Q : 所以 TypeScript 編譯器就無法檢查 ClockConstructor interface 了嗎 ?

16 行

1
2
3
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
1
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {

createClock() 為 constructor function (OOP 稱為 factory method),第 1 個參數為 class,其型別為 ClockConstructor interface,之後的參數為 constructor 的 signature。

當第 1 個參數傳入 class 後,TypeScript 就會在此檢查該 class 是否有實踐 ClockConstructor 所定義的 constructor interface。

Constructor interface 不能直接使用 implements,必須要搭配 constructor function / factory method,在其 parameter 使用 constructor interface,如此 TypeScript 就會在此檢查 class 是否有實踐 constructor interface。

1
return new ctor(hour, minute);

ctor 實踐了 ClockConstructor 這個 constructor interface,因此可以直接 new 來建立 object。

20 行

1
2
let clock = createClock(Clock, 12, 17);
clock.tick();

createClock 第 1 個參數傳入 Clock,注意 Clock 是個 class,不是 object。

createClock() 為典型的 constructor function,要使用 OOP 的 ClockFactory.create() 或 FP 的 createClock() ,實務上兩種都可以。

目前 TypeScript 的 class level 的 interface 只有 constructor interface,並沒有描述 static property 與 static method 的 interface。

Function Interface


定義 function 的 signature

  • 在 OOP,我們期望 class 該有哪些 public property 與 method,可使用 class interface 描述。
  • 在 FP,我們會期望 function 該有哪些 signature,可使用 function interface 描述。

但傳統 OOP 的 class interface 無法滿足 FP 的需求,因而有了 function interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ILogistics {
(weight: number): number
}

class ShippingService {
calculateFee(weight: number, logistics: ILogistics): number {
return logistics(weight);
}
}

const shippingService = new ShippingService();
const fee = shippingService.calculateFee(10, weight => 100 * weight + 10);

console.log(fee); // 1010

第 8 行

1
2
3
calculateFee(weight: number, logistics: ILogistics): number {
return logistics(weight);
}

calculateFee() 的第 2 個參數希望傳入一個 arrow function,其 signature 為 number =>number

第 1 行

1
2
3
interface ILogistics {
(weight: number): number
}

定義 ILogistics function interface,其 signature 為 (weight: number): number

12 行

1
const fee = shippingService.calculateFee(10, weight => 100 * weight + 10);

Arrow function 為 weight =>100 * weight + 10,TypeScript 編譯器會檢查此 arrow function 是不是符合 ILogistics interface。

JavaScript 目前都無法對 arrow function 的 signature 做檢查,但有了 function interface 之後,就能在編譯階段檢查 function 的 signature。

Hybrid Interface


由於 JavaScript 的動態語言特性,ES5 的 function 可以同時是 function,也是 object,我們可以使用 hybrid interface 來定義。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

第 1 行

1
2
3
4
5
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

Counter interface 同時具有 function 與 object 特性 :

  1. Function 的 signature 為 (start: number): string
  2. Object 有 interval property 與 reset() method

第 7 行

1
2
3
4
5
6
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}

同時設定 counter 有 function 與 object 部分。

14 行

1
2
3
4
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

c 可當 function,也可當 object 使用。

Hybrid interface 主要用於相容 ES5 與 3rd-party 套件,新寫的 TypeScript 與 Angular 則不建議使用 hybrid interface。

Inheritance


Interface Extending Interface

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

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

Square interface 繼承了 Shape interface,因此 squre 同時有 colorsideLength property。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

Square interface 同時多重繼承了 ShapePenStroke,因此 Squre 除了有自己的 sideLength 外,同時也具備了 ShapecolorPenStrokepenWidth

由於 interface 可以繼承,甚至多重繼承,因此設計 interface 時,可以遵循 介面隔離原則 : 使用者不該使用用不到 method 的 interface ,將 interface 開的小小的,再根據需求去組合 interface,讓物件與物件之間的耦合達到最小。

Interface Extending Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() { }
}

class TextBox extends Control {

}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}

class Location {
}
}

第 1 行

1
2
3
4
5
6
7
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

當 interface 繼承 class 時,有兩個特色 :

  1. 原 Class 原本的實作部分完全捨棄,只繼承 signature 部分
  2. 原 Class 的 private 與 protected 部分也會一併被繼承保留

所以 SelectableControl interface 除了有 select() 外,也會有 private state property。

1
2
3
class Button extends Control implements SelectableControl {
select() { }
}

Button 因為繼承了 Control,所以有 private state property,符合 SelectableControl interface 的要求。

17 行

1
2
3
4
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}

Image 沒有繼承 Control class,因此沒有 private state property,因此不符合 SeletableControl interface 的要求,TypeScript 編譯會報錯。

Q : 實務上該如何使用 interface 繼承 class ?

一開始就能由 TDD 逼出 interface 是最理想的,這種 interface 來自於需求,所開出的 interface 會最精準,但實務上還是會遇到一開始沒有 interface,事後需要抽 interface 需求,此時可以新增 interface 繼承目前的 class,將來新的 class 再使用 adapter 轉成目前的 interface,則原本的程式碼不用修改,達成開放封閉原則的要求。

1
2
3
4
5
6
7
8
9
10
class AWSSDK {
putObject(container: string, blob: string, file: string) {
}

getObject(container: string, blob: string) {
}

deleteObject(container: string, blob: string) {
}
}

一開始只有 AWSSDK 需求,因此 service 直接使用 AWSSDK class。

因為需求改變,需要使用 AzureSDK,因為 service 已經直接使用 AWSSDK,為了讓原本使用 AWSSDK 的 service 不要改 code,我們會由 AWSSDK class 去抽 interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface CloudSDK {
putObject(container: string, blob: string, file: string);
getObject(container: string, blob: string);
deleteObject(container: string, blob: string);
}

class AWSSDK implements CloudSDK {
putObject(container: string, blob: string, file: string) {
}

getObject(container: string, blob: string) {
}

deleteObject(container: string, blob: string) {
}
}

class AzureSDK implements CloudSDK {
putObject(container: string, blob: string, file: string) {
}

getObject(container: string, blob: string) {
}

deleteObject(container: string, blob: string) {
}
}

第 1 行

1
2
3
4
5
interface CloudSDK {
putObject(container: string, blob: string, file: string);
getObject(container: string, blob: string);
deleteObject(container: string, blob: string);
}

傳統 OOP 會使用重構工具 (WebStorm/PhpStorm/Resharper) 去 extract interface,產生 CloudSDK

第 7 行

1
2
class AWSSDK implements CloudSDK
class AzureSDK implements CloudSDK

然後所有的 class 去 implement interface,配合 DI 與 provider,則 service 不用做任何修改,達成開放封閉原則

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface CloudSDK extends AWSSDK {
}

class AWSSDK implements CloudSDK {
putObject(container: string, blob: string, file: string) {
}

getObject(container: string, blob: string) {
}

deleteObject(container: string, blob: string) {
}
}

class AzureSDK implements CloudSDK {
putObject(container: string, blob: string, file: string) {
}

getObject(container: string, blob: string) {
}

deleteObject(container: string, blob: string) {
}
}

第 1 行

1
2
interface CloudSDK extends AWSSDK {
}

CloudSDK interface 直接繼承 AWSSDK class 即可,不必再使用重構工具從 AWSSDK 去抽 interface。

剩下的程式碼完全一樣。

Conclusion


  • 因為 JavaScript 沒有 interface,TypeScript 為了讓 JavaScript 擁有強型別能力而補上 interface,但 TypeScript 的 interface 的完整度已經超過大部分語言。
  • TypeScript 的 object interface,其本質類似弱型別 PHP 的 duck typing 與 C# 4.0 的 dynamic,只是明確地將 property signature 型別化,並且提前在編譯階段進行檢查,但 object literal 是例外,會啟動 excess property checks。
  • 因為 TypeScript 有 object interface,所以 model 的型別,一般 OOP 會使用 class,但 TypeScript 會使用 interface。
  • Constructor interface 為 TypeScript 所獨創,以往 OOP 的 class interface 無法定義constructor,但 constructor interface 則可對 constructor 加以定義,但不能使用 implements,而要改用 constructor function / factory method,在 parameter 使用 constructor interface,如此 TypeScript 編譯器就能對 constructor signature 加以檢查。
  • Interface 可直接繼承 class,因此 adapter pattern 會有更精簡的實作方式。

Reference


TypeScript, Handbook : Interfaces

2018-02-19