問題是什麼?
TS 的進階部分——類型操作,到底哪些部分是在業務開發中用得上的技巧?我們來列舉實際問題來看看。
類型變換
「枚舉」變成「聯合」
當我們製作組件的時候,為了避免重複,一些字符類型的變量,用枚舉來創建是十分合適的。比如一個日期組件裏定義星期一到三:
enum Weekday {
MON = 'monday',
TUE = 'tuesday',
WED = 'wednesday'
}
這樣無論在渲染還是計算的時候,我們都能用 Weekday.MON 來避免重複和寫錯單詞。但是在使用組件的時候,導出的屬性卻不能正確地提示類型:
interface DayProps{
name: Weekday
}
<Day name='monday'/> // ⚠️ String 'monday' cannot be used to enum type Weekday
此時,除了從組件庫導出 Weekday 的方式之外,還能通過創建“字符串字面量聯合類型(String literal union type from enum)”的方式解決:
interface DayProps{
name: `${Weekday}`
}
<Day name='monday'/> // Day.name: ('monday'|'tuesday'|'wednesday')
「對象」變成「聯合」
單個配置可以用枚舉代替對象,但如果是多個配置合成一個配置的時候,就只能用對象了。比如
const workdays = {
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5
}
const weekends = {
Sat: 6,
Sun: 7
}
const weekdays = { ...workdays, ...weekends }
然後要正確地提示到“星期幾”的值,可以先用 keyof 封裝一個 valueOf<T>,方便我們的操作。
type valueOf<T> = T[keyof T];
type Weekday = valueOf<typeof weekdays> // Weekday: string
從上面可以看到,Weekday 只解析成了 string,並不是我們期待的,精確的取值範圍 1|2|3...|7。原來是 TS 只能對 readonly 的類型或者數據進行精確解析,所以我們需要定義變量的時候,聲明它們是隻讀的類型。
const workdays = {
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5
} as const // 轉變為“只讀”類型,讓其值可以被正確地解析
const weekends = {
Sat: 6,
Sun: 7
} as const
const weekdays = { ...workdays, ...weekends }
type Weekday = valueOf<typeof weekdays> // Weekday: (1|2|3|4|5|6|7)
「數組」變成「interface」
當我們創建一個,各種元素並不怎麼相關,僅僅只是用途相同的集合的時候,會用到字符串數組。比如 icon 圖片的數組
const icons = ['banana.png', 'avata.svg', 'water.jpg']
// 由於圖片自帶名字,所以直接來生成對象使用
const iconCollection = icons.reduce((acc, path) => {
const name = path.split('.')[0]
return Object.assign(acc, { [name]: path })
}, Object())
/*{
banana: "banana.png",
avata: "avata.svg",
water: "water.jpg"
}*/
然後我們想正確地提示 iconCollection 的類型,是否可以用上面 as const 的技巧,把它轉換成只讀類型呢?
這是不行的!因為它是動態地生成的對象,無法被靜態地解析。要解析,只能是對靜態的 icons 數組下手,把它轉換成 interface。
const icons = ['banana.png', 'avata.svg', 'water.jpg'] as const // Trans to readonly
type SplitName<T> = T extends `${infer P}.${string}` ? P : never; // 利用類型推導(infer),得到文件名 P
type IconCollection = Record<SplitFileName<(typeof icons)[number]>, string> // IconCollection: {banana: string, avata: string, water: string}
函數的精確類型提示
重載
一個好的函數,最好就是單一職責,且一種輸入,對應一種輸出。但有時候確實會有,一個功能處理不同數據類型的情況,比如以下這個函數
/**
* 改變參數類型,數字轉字符串,字符串則轉數字
* @param x
*/
function changeType(x: string|number): number|string {
return typeof x === 'string' ? Number(x) : String(x)
}
這個類型聲明雖然沒有錯誤,但並不能得到精準的提示。我們希望類型提示,與函數描述完全一致,這時候重載就上場了。
function changeType(x: number): string;
function changeType(x: string): number;
/**
* 改變參數類型,數字轉字符串,字符串則轉數字
* @param x
*/
function changeType(x) {
return typeof x === 'string' ? Number(x) : String(x)
}
changeType(123) // function changeType( x: number): string
changeType('456') // function changeType( x: string): number
類型分發
繼續沿用上面的例子,我們可以用類型分發(distribution)來根據參數類型,推導輸出類型。
/**
* 改變參數類型,數字轉字符串,字符串則轉數字
* @param x
*/
function changeType<T>(x: T): T extends number ? string : T extends string ? number : never {
return typeof x === 'string' ? Number(x) : String(x)
}
最後
我發現的業務項目中常見的 TS 進階用法就以上這些,大家還有什麼補充的呢?歡迎評論。