TypeScript開發小狀況記錄之選且隻選一個
前言
在項目內,使用TypeScript的過程中遇到瞭一些狀況、報錯或棘手的情況,決定開一個系列記錄遇到的問題和我的解決方法。
背景
在項目開發中,很多時候會遇到一種場景,需要定義一個對象的類型,此類型必須包含某n個字段中的其中一種。
例如,我要定義一個工程師(Engineer)的對象,裡面包括姓名(name),性別(gender),年齡(age)和一門編程語言(java/cpp/go/js四選一)的評價。
顯然,前三個字段都是很簡單的,但是第四個就有點麻煩瞭。首先,第四個字段的key是可以不一樣(甚至value也有可能不同),其次字段隻能從給定的裡面4選1。
初步方案
一開始是考慮使用可選或聯合類型,但是發現沒有辦法進行4選1的限制,對於沒有編程語言字段,或者多個編程語言字段的情況並沒有很好的限制。最後隻能使用泛型,再使用時進行顯式的聲明。
於是,類型定義如下:
interface ICodingLangRating { java: string cpp: string go: string js: string } type Engineer<K extends keyof ICodingLangRating> = { name: string gender: 'male' | 'female' age: number } & Pick<ICodingLangRating, K>
對該聲明的校驗代碼如下:
// 正確 const candidate: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22, java: 'fabulous' } // 錯誤,聲明瞭java,但是卻同時定義瞭java和go字段 const candidate_1: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22, java: 'fabulous', go: 'not bad' } // 錯誤,聲明瞭java,但是類型不正確 const candidate_2: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22, java: 666 } // 錯誤,聲明瞭java,但是卻定義瞭go字段 const candidate_3: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22, go: 'not bad' } // 錯誤,聲明瞭java,但是卻同時定義瞭cpp和go字段 const candidate_4: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22, cpp: 'unknown', go: 'not bad' } // 錯誤,聲明瞭java,但是卻沒有定義java字段 const candidate_5: Engineer<'java'> = { name: 'Jack', gender: 'male', age: 22 } // 錯誤,聲明瞭ICodingLangRating中不存在的python const candidate_6: Engineer<'python'> = { name: 'Jack', gender: 'male', age: 22, python: 'just so so' }
從校驗代碼可以看出,針對各種不符合期望的情況:
- 聲明a,卻定義a和b
- 聲明a,但定義a的類型不正確
- 聲明a,卻定義b
- 聲明a,卻定義b和c
- 聲明a,卻沒有定義a
- 聲明不合法的f
都能做出正確的限制,確保在業務場景的代碼中,有且隻有一個合法范圍的字段。
但是,轉折來瞭!
在後來的使用中,我們發現,其實這個解決方案隻是一個弱限制,如果在泛型的顯式聲明中,傳入聯合類型的話,那還是可以繞過有且隻有一個編程語言字段的限制。
// 正確,聲明瞭java和go,並同時定義瞭java和go字段 const candidate_1: Engineer<'java' | 'go'> = { name: 'Jack', gender: 'male', age: 22, java: 'fabulous', go: 'not bad' }
難道就真的沒有辦法做到隻能選擇一個的限制麼?
終極方案
根據上面的嘗試,目前我們還缺少的是如何阻止同時有2個或以上的合法字段出現。最笨的方式就是為每一個語言都定義一個類似{ langName: string }
這樣的類型然後通過extends或者聯合類型使用,但是顯然這樣就沒有辦法做到在其它情況通用。
而通過官方現成的工具類型,由於都是支持字面量和聯合類型,沒有辦法篩選出隻包含一個字段的類型。就在這時,我想到,是不是可以定義出一個類型,包含全部字段,但是隻有一個字段是正確有意義,其他字段都是無意義的呢。
最終,我就構造出下面這個PickOne工具類型:
type PickOne<T> = { [K in keyof T]: Record<K, T[K]> & Partial<Record<Exclude<keyof T, K>, undefined>> }[keyof T]
測試代碼如下:
type OneLang = PickOne<ICodingLangRating> // 正確 const lang: OneLang = { java: 'good' } // 錯誤 const lang2: OneLang = { python: 'unknown' } // 錯誤 const lang3: OneLang = { java: 'good', go: 'good' } // 錯誤 const lang4: OneLang = { java: 123 }
最後,類型定義代碼如下:
interface ICodingLangRating { java: string cpp: string go: string js: string } type Engineer = { name: string gender: 'male' | 'female' age: number } & PickOne<ICodingLangRating>
使用瞭這個PickOne工具類型,我不需要在使用的時候顯式的指定編程語言,甚至還能在其它類似的場景使用。
總結
到此這篇關於TypeScript開發小狀況記錄之選且隻選一個的文章就介紹到這瞭,更多相關TypeScript選且隻選一個內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 不需要用到正則的Python文本解析庫parse
- TypeScript保姆級基礎教程
- typescript快速上手的基礎知識篇
- TypeScript基本類型之typeof和keyof詳解
- TypeScript中定義變量方式以及數據類型詳解