請看下面的例子,並試想為什麼會出現 Error?
const user = {
name: 'test',
age: 18,
gender: 'female'
}
const keys = Object.keys(user)
keys.forEach(key => {
if (user[key] == null) {
//
}
})
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'UserData'.
No index signature with a parameter of type 'string' was found on type 'UserData'.(7053)
如果你只是單純想知道怎麼解決上面的 Error,可以這麼改寫:
const keys = Object.keys(user) as (keyof typeof user)[]
const user = {
name: 'test',
age: 18,
gender: 'female'
}
const keys = Object.keys(user) as (keyof typeof user)[]
keys.forEach(key => {
if (user[key] == null) {
//
}
})
但這個錯誤似乎很荒謬,keys
明明是 user 的所有 property 為什麼遍歷 keys 卻無法知道 key 是什麼型別?
這就需要談到 Object.keys 的型別定義了。
keys(o: object): string[]
,可以看到返回值為 string[]
如果定義改為 keys<T extends object>(o: T): (keyof T)[];
就能解決這個問題。這樣定義 Object.keys 似乎不費吹灰之力,但 TypeScript 卻不這樣做。原因與 TypeScript 的結構型態(Structural typing)有關。
TypeScript 中的結構型態
這就不得不提到 鴨子型別 Duck Typing,在程式設計中是動態型別的一種風格。在這種風格中,一個物件有效的語意,不是由繼承自特定的類或實現特定的介面,而是由「當前方法和屬性的集合」決定。
靜態型別:宣告變數時必須指定明確的型別,且型別一旦宣告後在程式執行時無法任意更換型別。如:JAVA, C, C++, C#…
動態型別:宣告變數時不用明確指定型別,且在程式執行時可以任意更換型別。如:JS, Python, PHP…
鴨子型別的名字來源於由詹姆斯·惠特科姆·萊利提出的鴨子測試,「鴨子測試」可以這樣表述:
當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。
JavaScript 就是典型的鴨子型別語言。而 TypeScript 的結構型態是建立在鴨子型別的基礎之上,即 A 型別如果要兼容 B 型別,那麼 B 中至少要有和 A 相同的屬性。
舉個例子,假設有一個型別 UserData
,表示一個包含 name
和 age
兩個屬性的物件。當我們擁有一個型別為 UserData
的物件時,我們只知道它至少包含了 name
和 age
這兩個屬性,但我們無法確定是否還有其他屬性。
這就是所謂的「至少包含」的概念,而不是「確定完全等同於」,因此 saveUser(user1)
會出現 Error。
interface UserData {
name: string
age: number
}
const saveUser = (user: UserData) => {
...
}
const user1 = { name: 'Test' }
const user2 = { name: 'John', age: 20, gender: 'male' }
saveUser(user1)
saveUser(user2)
不安全的使用 Object.keys
如果今天需要寫一個方法來遍歷物件中的每個屬性是否通過驗證,可以使用 Object.keys
:
function validateUser(user: User) {
let error = "";
for (const key of Object.keys(user)) {
const validate = validators[key];
error ||= validate(user[key]);
}
return error;
}
但此方法的問題在於,該物件可能包含 validators
中不存在的屬性。
interface User {
name: string;
password: string;
}
const validators = {
name: (name: string) => name.length < 1
? "Name must not be empty"
: "",
password: (password: string) => password.length < 6
? "Password must be at least 6 characters"
: "",
};
function validateUser(user: User) {
let error = "";
for (const key of Object.keys(user)) {
const validate = validators[key];
error ||= validate(user[key]);
}
return error;
}
const user = {
name: 'Alex',
password: '1234',
email: "alex@example.com",
};
validateUser(user);
要解決這個 Error 也很簡單,定義型別為 Array<keyof User>
即可:
function validateUser(user: User) {
let error = "";
for (const key of Object.keys(user) as Array<keyof User>) {
const validate = validators[key];
error ||= validate(user[key]);
}
return error;
}
但通過前面的說明,現在應該很容易理解為什麼上面例子中使用 Object.keys
會出現錯誤了。
善加利用結構型態
假設現在有一個函數 getKeyboardShortcut
,該函數有一個參數 e
型別為 KeyboardEvent
function getKeyboardShortcut(e: KeyboardEvent) {
if (e.key === "s" && e.metaKey) {
return "save";
}
if (e.key === "o" && e.metaKey) {
return "open";
}
return null;
}
寫一些 unit test 驗證一下
expect(getKeyboardShortcut({ key: "s", metaKey: true }))
.toEqual("save");
expect(getKeyboardShortcut({ key: "o", metaKey: true }))
.toEqual("open");
expect(getKeyboardShortcut({ key: "s", metaKey: false }))
.toEqual(null);
此時會出現 Error,因為你必須傳入 KeyboardEvent 的 37 個 props 才行。
Type '{ key: string; metaKey: true; }' is missing the following properties from type 'KeyboardEvent': altKey, charCode, code, ctrlKey, and 37 more.
雖然可以使用 as KeyboardEvent
來解決這個 Error,但是可能會掩蓋可能發生的其他型別錯誤。
getKeyboardShortcut({ key: "s", metaKey: true } as KeyboardEvent);
所以我們可以自定義一個 interface KeyboardShortcutEvent
,只包含我們需要傳入的 KeyboardEvent
中的其中兩個參數。這是可行的,因為 KeyboardEvent 是 KeyboardShortcutEvent 的超集。
interface KeyboardShortcutEvent {
key: string;
metaKey: boolean;
}
function getKeyboardShortcut(e: KeyboardShortcutEvent) {}
window.addEventListener("keydown", (e: KeyboardEvent) => {
const shortcut = getKeyboardShortcut(e); // This is OK!
if (shortcut) {
execShortcut(shortcut);
}
});