TypeScript - Advanced Object Types
學習資源:
Introduction
在寫 TypeScript 一個常見的挑戰是如何在程式碼會遇到的狀況下使用型別,比如下面的例子:
class Robot {
identify(id: number) {
console.log(`beep! I'm ${id}`);
}
}
在上面宣告一個 class 叫做 Robot,但要怎麼去使用它?並且,一些機器人可能比其他機器人有更多的功能,或者具有變化的屬性名稱。在這些情況下,要如何去應用型別?
所以這篇文章會著重在如何把型別應用在這些狀況上。並且也會深入了解如何把型別應用在物件導向上。
Interfaces and Types
在 TypeScript 除了可以應用 type 來定義型別,可以使用 interfaces。
使用 type 如下:
type Mail = {
postagePrice: number;
address: string;
}
const catalog: Mail = ...
使用 interfaces 如下:
interface Mail {
postagePrice: number;
address: string;
}
const catalog: Mail = ...
以上兩者除了語法上有些不同,在功能上其實一樣。
而最大的不同是,interfaces 只能用來做 object 的型別註釋,而 type 則可以使用在 object、primitives 等等的型別。目前看來 type 感覺比 interfaces 實用很多,那為什麼要使用 interfaces 呢?
理由是這樣子的,有時,我們不想要一個能做所有事情的型別。會希望我們的型別受到某些限制,這樣的限制可以讓我們寫一個風格比較一致的程式。而由於 interfaces 只能註釋 object,因此它非常適合編寫物件導向的程式。
Interfaces and Classes
TypeScript 常使用 interfaces
與 class
來做搭配使用。讓我們能夠使用 implements
將型別應用於 object / class 上,例如:
interface Robot {
identify: (id: number) => void;
}
class OneSeries implements Robot {
identify(id: number) {
console.log(`beep! I'm ${id.toFixed(2)}.`);
}
answerQuestion() {
console.log('42!');
}
}
以上的例子有一些細節要注意。因為 Robot
自己有 .identify()
的方法,而 Robot 應用於 OneSeries
,所以 OneSeries
一定要有 .identify()
的方法。而 OneSeries
也可以有自己的方法 answerQuestion()
。
藉由使用 interfaces
與 class
可以讓型別在應用上有更多彈性,並且很好地實現物件導向的方式。
Deep Types
有時當程式碼愈來愈複雜的時候,會需要新增更多的方法與屬性到object 中,以至於讓我們會讓這個巢狀結構愈來愈深:
class OneSeries implements Robot {
about;
constructor(props: { general: { id: number; name: string; } }) {
this.about = props;
}
getRobotId() {
return `ID: ${this.about.general.id}`;
}
}
在這個 class 中,OneSeries
期望有一個在 object 中的 object 的屬性 about
。而在 getRobotId()
中,OneSeries
會返回 this.about.general.id
,所以為了註釋在 object 中的 object 型別,我們可以使用像以下的 interfaces
:
interface Robot {
about: {
general: {
id: number;
name: string;
};
};
getRobotId: () => string;
}
Composed Types
隨著資料的巢狀結構越來越深,會導致 object 變得難以寫入和讀取。比如以下型別:
interface About {
general: {
id: number;
name: string;
version: {
versionNumber: number;
}
}
}
這種型別有兩個層的巢狀結構。這可能適用於一個簡短的程式,但隨著我們程式的擴充套件和我們需要的功能變多,我們可能會遇到兩個問題:
- 隨著我們新增更多資料,這個
interface
可能會有更多巢狀結構,以至於我們自己和其他開發人員很難閱讀。 - 有可能在使用的時候,只想要這種型別的部分 object。比如,可能只想要
version
這個 object,而不想要其他部分。
為了解決這個問題,TypeScript 可以讓我們組合型別。可以藉由定義多種型別,並在其他型別中引用它們。比如:
interface About {
general: General;
}
interface General {
id: number;
name: string;
version: Version;
}
interface Version {
versionNumber: number;
}
雖然產生的程式稍長一些,但我們現在可以更輕鬆地讀取程式,並且可以在程式中的其他地方重複使用較小的 interface
。
Extending Interfaces
在 TypeScript中,組合型別的方式不一定總是足夠的。有時,如果能將所有型別成員從一個型別複製到另一個型別就會很方便。所以這時可以使用extension
來完成,比如:
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
const mySquare: Square = { sideLength: 10, color: 'blue' };
使用 extension
可以幫助我們管理程式,方法是將提取共同型別成員到他們自己的 interface
中,然後將它們複製到更特別的型別。
Index Signatures
有時在再輸入 object 型別的資料時,並不會知道 object 裡的屬性名稱。比如從 API 獲取資料時。而雖然不知道屬性名稱是什麼,但屬性是什麼型別以及 value 是什麼型別是可以預先知道的。因此在為 object 做型別註釋的時候,可以用一個變數先來代表屬性的名字,這樣的方式就叫 Index Signatures。
比如說,我們查詢地圖API,獲得可以檢視日食的緯度列表。資料可能看起來像:
{
'40.712776': true;
'41.203323': true;
'40.417286': false;
}
而如同上面所示,屬性名稱會是 string, value 會是 booleans,儘管我們不知道那時實際的屬性名字會是什麼。為了幫這樣的 object 標記型別,用 Index Signatures 來做的話:
interface SolarEclipse {
[latitude: string]: boolean;
}
這樣的方式不僅可以通用,閱讀性上也比較好。
Optional Type Members
interface
所標的物件型別中的成員不是每個都是必須要提供的,其實也可以是有選擇的。比如以下:
interface OptionsType {
name: string;
size?: string;
}
function listFile(options: OptionsType) {
let fileName = options.name;
if (options.size) {
fileName = `${fileName}: ${options.size}`;
}
return fileName;
}
就像以上的 size?
所標示的,表示 size 是可以不提供的。也就是說像下面在呼叫 listFile()
時,可以不提供關於 size 的資訊。
listFile({ name: 'readme.txt' })