TypeScript 中限制物件鍵名的取值範圍

劉哇勇發表於2021-05-19

當我們使用 TypeScript 時,我們想利用它提供的型別系統限制程式碼的方方面面,物件的鍵值,也不例外。

譬如我們有個物件儲存每個年級的人名,型別大概長這樣:

type Students = Record<string, string[]>;
 

理所當然地,資料就是長這樣:

const students: Students = {
  Freshman: ["David", "John"],
  sophomore: [],
  Junior: ["Lily"],
  Senior: ["Tom"],
};
 

限制物件鍵名為列舉

上面資料型別的問題是,年級是有限的幾種可值取,而該物件上可任意新增屬性,這樣顯得資料不夠純粹。

所以我們新增列舉,列出可取的值:

export enum Grade {
  Freshman,
  sophomore,
  Junior,
  Senior,
}
 

現在,把物件的鍵名限制為上面列舉就行了。

- type Students = Record<string, string[]>;
+ type Students = Record<Grade, string[]>;
 

這樣我們的資料可寫成這樣:

const students: Students = {
  [Grade.Freshman]: ["David", "John"],
  [Grade.sophomore]: [],
  [Grade.Junior]: ["Lily"],
  [Grade.Senior]: ["Tom"],
  // ❌ Object literal may only specify known properties, and 'blah' does not exist in type 'Students'.ts(2322)
  blah: ["some one"],
};
 

這樣,限制住了物件身上鍵名的範圍,可以看到如果新增一個列舉之外的鍵會報錯。

更加語義化的列舉值

但上面的做法還是有不妥之處,因為列舉值預設是從 0 開始的數字,這樣,作為鍵值就不夠語義了,這點從訪問物件的屬性時體現了出來:

1

修正我們的列舉,用更加語義的文字作為其值:

export enum Grade {
  Freshman = "Freshman",
  sophomore = "sophomore",
  Junior = "Junior",
  Senior = "Senior",
}
 

此時再使用該列舉時,得到的就不是無意義的數字了。

2

如果你願意,列舉值也可以是中文,

export enum Grade {
  Freshman = "大一萌新",
  sophomore = "大二學弟",
  Junior = "大三學妹",
  Senior = "大四老司機",
}
 

使用時也是沒任何問題的:

3

鍵值可選

上面的型別定義還有個問題,即,它要求使用時物件包含列舉中所有值,比如 sophomore 這個年級中並沒有人,可以不寫,但會報錯。

// ❌ Property 'sophomore' is missing in type '{ Freshman: string[]; Junior: string[]; Senior: string[]; }' but required in type 'Students'.ts(2741)
const students: Students = {
  [Grade.Freshman]: ["David", "John"],
  // [Grade.sophomore]: [],
  [Grade.Junior]: ["Lily"],
  [Grade.Senior]: ["Tom"],
};
 

所以,優化型別為可選:

type Students = Partial<Record<Grade, string[]>>;
 

限制物件的鍵名為陣列中的值

假若可選的值不是通過列舉定義,而是來自一個陣列,

const grades = ["Freshman", "sophomore", "Junior", "Senior"];
 

這意味著我們需要提取陣列中的值形成一個聯合型別。

首先利用const assertions 把陣列轉元組(Tuple)型別,

const grades = <const>["Freshman", "sophomore", "Junior", "Senior"];
 

再利用 typeofLookup Types 得到最終的聯合型別:

// 實際為 type Keys = "Freshman" | "sophomore" | "Junior" | "Senior"
type Keys = typeof grades[number];
 

最後資料型別和資料可寫成:

type Students = Partial<Record<Keys, string[]>>;

const students: Students = {
  Freshman: ["David", "John"],
  Junior: ["Lily"],
  Senior: ["Tom"],
};
 

須知這種形式下,物件的 key 與原陣列中元素其實沒有語法層面的關聯,即,編輯器的「跳轉定義」是不可用的。

Screen Shot 2021-05-19 at 3 29 27 PM

儘量還是保持程式碼之間的關聯才能體現出 TypeScript 的作用,所以像這種只有型別約束而無法建立關聯的操作是不建議的。

相關資源

The text was updated successfully, but these errors were encountered:

相關文章