博客 / 詳情

返回

【Typescript】業務開發中常用進階技巧

問題是什麼?

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 進階用法就以上這些,大家還有什麼補充的呢?歡迎評論。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.