TypeScript 與物件型別
這篇文章會聚焦在 TypeScript 型別上,所以不會提到太多 JavaScript 的基本觀念,
如果不熟悉 JavaScript 的物件 (Object),可以參考 MDN - Object。
物件 Object
在 JavaScript 中,我們可以透過 {}
來直接建立物件,
稱為 物件初始化 (Object Initializer)。
const vec = {
// ^? const vec: { x: number; y: number; }
x: 0,
y: 0,
};
let vec = {
//^? let vec: { x: number; y: number; }
x: 0,
y: 0,
};
在上面的範例中,我們並沒有明確標註 vec
的型別,
透過 TypeScript 推斷出來的型別是 { x: number; y: number; }
。
注意,使用 let
或是 const
也是會影響到型別資訊。
在接下來的範例,也會分別提供使用 let
跟 const
後得到的型別。
關於 TypeScript 的型別推斷 (Type Inference) 可以參考 TypeScript 如何給予型別。
語法 Syntax
以下示範 物件型別 的型別標註。
{ x: number; y: number; }
{ x: number; y: number }
{ x: number, y: number, }
{ x: number, y: number }
上面可以看到,
我們標注了一個具有兩個屬性 (properties) 的型別,
分別是 x
跟 y
,而且都是 number
型別。
我們可以使用 ,
或是 ;
來分隔屬性,
而且最後一個分隔符號可以選擇性忽略。
型別標註 Type Annotation
以下示範 物件型別 的型別標註。
const vec: { x: number; y: number; } = {
x: 0,
y: 0,
};
let vec: { x: number; y: number; } = {
x: 0,
y: 0,
};
同理,我們可以用同樣的方式來標註函式參數 (Function Parameter) 以及 函式回傳值 (Function Return Value)。
declare function move(vec: { x: number; y: number; }): { x: number; y: number; };
move({ x: 0, y: 0 });
const vec1 = { x: 0, y: 0 };
move(vec1);
let vec2 = { x: 0, y: 0 };
move(vec2);
型別別名 與 介面 的物件型別標註法
當然,我們可以直接針對每個 variable 進行個別物件型別標註,
但透過 型別別名 (Type Alias) 跟 介面 (Interface) 將型別標註獨立出來,
可以讓多個 variable 可以共用同一個型別標註。
以下是 Type Alias 的寫法。
type Vec = { x: number; y: number; };
const vec: Vec = {
x: 0,
y: 0,
};
let vec: Vec = {
x: 0,
y: 0,
};
declare function move(vec: Vec): Vec;
以下是 Interface 的寫法。
interface Vec {
x: number;
y: number;
}
const vec: Vec = {
x: 0,
y: 0,
};
let vec: Vec = {
x: 0,
y: 0,
};
declare function move(vec: Vec): Vec;
屬性型別 Property Type
在物件中每個屬性也都有自己的型別,
TypeScript 會特別在型別資訊上標註 (property) 來表示這是一個屬性。
const vec = {
x: 0,
// ^? (property) x: number
y: 0,
// ^? (property) y: number
};
型別檢查 Type Checks
型別檢查 (Type Checks) 是 TypeScript 重要的功能之一,
認識錯誤訊息 (Error Message) 是了解這套語言的重要一環。
以下會介紹一些常見的物件型別檢查錯誤訊息。
重複屬性 Duplicate Property
在 JavaScript 物件初始化 時,屬性是可以重複定義的。
但 TypeScript 會拋出錯誤訊息 1117
。
type Vec = { x: number; y: number; };
const vec: Vec = {
x: 0,
y: 0,
x: 0,
//^ An object literal cannot have multiple properties with the same name.(1117)
};
以開發的角度來看,這個錯誤訊息是很合理的,
在物件初始化時,定義屬性重複大多數是開發者的失誤,
透過這道檢查,可以避免開發者定義無意義的重複屬性,
以避免誤導後續維護者的可能。
多餘屬性 Excess Property Checks
在物件初始化時,開發者定義的屬性超出型別所定義的屬性時,
TypeScript 會拋出錯誤訊息 1117
。
type Vec = { x: number; y: number; };
const vec: Vec = {
x: 0,
y: 0,
z: 0,
// ^ Object literal may only specify known properties, and 'z' does not exist in type 'Position'.(2353)
};
let vec: Vec = {
x: 0,
y: 0,
z: 0,
// ^ Object literal may only specify known properties, and 'z' does not exist in type 'Position'.(2353)
};
關於多餘屬性的檢查,以下來討論一個常見的問題。
請問下面的 move(vec)
會拋錯嗎?
interface Vec {
x: number;
y: number;
}
declare function move(vec: Vec): Vec;
const vec = {
x: 0,
y: 0,
z: 0,
};
// error ?
move(vec);
這邊提供 TypeScript Playground 供讀者參考。
同樣的,請問下面的 move({ x: 0, y: 0, z: 0 })
會拋錯嗎?
interface Vec {
x: number;
y: number;
}
declare function move(vec: Vec): Vec;
// error ?
move({ x: 0, y: 0, z: 0 });
這邊也提供 TypeScript Playground 供讀者參考。
為什麼 move(vec)
不會拋錯,但 move({ x: 0, y: 0, z: 0 })
卻會拋錯呢?
TypeScript 團隊認為,物件初始化階段多餘的屬性是多數情況屬於失誤,
避免錯誤發生會比起容許這個情況的效益來得大,
所以 TypeScript 會在物件初始化階段嚴格檢查多餘的屬性。
且透過 tsconfig suppressExcessPropertyErrors 選項,
開發者可以自行斟酌是否開啟這項檢查。
有興趣現在深入研究的讀者,這邊提供對應的 Issues 與 PR 以供參考。
以此類推,請問下面的 move_impl_1
跟 move_impl_2
哪一個會拋錯?
interface Vec {
x: number;
y: number;
}
function move_impl_1(): Vec {
const result = { x: 0, y: 0, z: 0 };
return result
}
function move_impl_2(): Vec {
return { x: 0, y: 0, z: 0 }
}
如果我們真的想要讓物件初始化時可以有多餘的屬性,
我們可以使用 Index Signature 明確的告知這個物件可以接受任意屬性名 (property key)。
在下面我們會介紹 Index Signature 的詳細用法。
interface Config {
x: number;
y: number;
[key: string]: number;
}
declare function defineConfig(config: Config): Config;
defineConfig({ x: 0, y: 0, z: 0 });
型別索引訪問 Indexed Access Types
因為語法很接近,所以這邊稍微提及一下。
在 TypeScript 中,
我們可以把型別本身當作一種值,
可以透過 []
來訪問型別中的屬性型別。
interface Vec {
x: number;
y: number;
}
type X = Vec['x'];
//^? type X = number
關於 Indexed Access Types,我們以後會在 TypeScript 進階型別討論。
屬性修飾符 Property Modifiers
在物件中,屬性可以透過修飾 (Modifiers) 來調整屬性的行為,
以下會介紹三種屬性修飾。
可選 Optional
Optional 譯作 可選 或是 可為空,
以下使用 可選 進行翻譯因為比較貼近實際情況。
語法 Syntax
很多時候,我們會遇到物件中的某個屬性不一定存在,
這時候我們可以在屬性名後面加上 ?
來表示該屬性可選。
type Vec = { x: number; y?: number; };
interface Vec {
x: number;
y?: number;
}
以下示範,當我們將 y
設定為可選時,
vec1
跟 vec2
都是合法的物件初始化。
type Vec = { x: number; y?: number; };
const vec1: Vec = {
x: 0,
};
const vec2: Vec = {
x: 0,
y: 0,
};
const vec3: Vec = {
// ^^^^^^^^^ Property 'x' is missing in type '{ y: number; }' but required in type 'Position'.(2741)
y: 0,
};
上面看到 vec3
會拋錯,
因為 x
是必要屬性,但 vec3
卻沒有定義 x
。
可選屬性型別 Optional Property Type
被標示成可選的屬性,其型別會被標示成 T | undefined
(T
是該屬性的型別)。
type Vec = { x: number; y?: number; };
declare const vec: Vec;
vec.x;
//^? (property) x: number
vec.y;
//^? (property) y?: number | undefined
vec.x + vec.y;
// ^^^^^ 'position.y' is possibly 'undefined'.(18048)
上面可以看到,
vec.y
的型別是 number | undefined
,
所以當我們在 vec.x + vec.y
嘗試使用 vec.y
時,
TypeScript 會拋出錯誤訊息 18048
,
試圖提醒開發者 vec.y
可能是 undefined
。
另外要注意,可選的屬性型別是 T | undefined
而不是 T | null
,
以下範例可以看到,當我們試圖將 null
賦值給可為空的屬性時,
TypeScript 會拋出錯誤訊息 2322
。
type Vec = { x: number; y?: number; };
declare const vec: Vec;
vec.y = null;
//^^^^^ Type 'null' is not assignable to type 'number | undefined'.(2322)
在 TypeScript 中,null
跟 undefined
是不同型別,
JavaScript 中所代表的意義也不同,
詳細可以參考 TypeScript 原始型別跟純量型別。
實務上,我通常不太去區分 null
跟 undefined
(因為沒必要又很麻煩),
我已經很習慣使用 nullish 的概念去處理空值的情況。
以後有機會我們會再深度探討。
如何妥善處理可選屬性
對於 可選屬性的檢查 以及 妥善的處理 可以大大減少程式的出錯的概率。