定義由特定字串之一組成的 TypeScript 陣列
我想定義一種類型,其中陣列可以包含特定字串列表中的一個,例如:
const foo = "foo";
const bar = "bar';
const baz = "baz':
// acceptable
[foo, bar]
// acceptable
[foo, bar, baz]
// unacceptable
[foo, foo]
// unacceptable
[foo, bar, bar]
我該怎麼做這個?
答案#1
我完全喜歡這種問題,即使答案的形式是「你可以做後空翻並接近解答,但可能不值得」這樣的。這個答案就是這樣。讓我們來做一些後空翻吧!
首先,嘗試表示一個約束 NoRepeats<T>,如果 T 的所有屬性類型都是不同的,那麼輸出將是 T,但如果即使有兩個鍵具有相同的屬性類型,輸出將是 never:
type NoRepeats<T> = true extends (
(keyof T) extends (infer K) ? K extends any ? (
T[K] extends T[Exclude<keyof T, K>] ? true : never
) : never : never
) ? "No Repeats Please" : T
它使用分佈式條件類型將 keyof T 拆解成單個鍵 K,然後比較 T[K] 與 T[Exclude<keyof T, K>],其中 Exclude<keyof T, K> 的意思是「除了 K 以外的所有其他鍵」。如果它們匹配,則整個結果將是 "No Repeats Please"(相當於用來替代無效類型的簡單錯誤訊息)。如果沒有匹配的,則答案是 T。
讓我們看看它的表現:
interface X {a: string, b: number, c: boolean}; // no repeats
declare const x: NoRepeats<X>; // x is of type X
interface Y {a: string, b: number, c: string}; // a and c are string
declare const y: NoRepeats<Y>; // y is of type "No Repeats Please"
現在,如果你傳入的是陣列類型,你不會想要檢查每一個可能的屬性是否有重複,因為陣列有像 forEach 和 map 這樣的方法,以及像 length 這樣的屬性。而且,更重要的是,你其實只想檢查元組,而不是一般的陣列。那是因為編譯器對陣列所知道的只是它所有元素類型的聯集,它無法判斷是否有重複。因此,你只會檢查元組的類數字鍵("0"、"1"、"2",直到但不包括元組的長度)。
所以我們需要移除所有對所有陣列類型都通用的鍵……我將稱之為「剝離」元組:
type StripTuple<T extends any[]> = Pick<T, Exclude<keyof T, keyof any[]>>
並確保我們只檢查元組,而不是一般陣列:
type NoRepeatTuple<T extends any[]> =
number extends T['length'] ? "Must Be Tuple" : NoRepeats<StripTuple<T>>
所以,讓我們在元組和陣列上試試這個:
declare const z: NoRepeatTuple<[string, number, boolean]> // {0: string, 1: number, 2:boolean}
declare const a: NoRepeatTuple<[string, number, string]> // "No Repeats Please"
declare const b: NoRepeatTuple<string[]> // "Must Be Tuple"
所以我們越來越接近了。現在無法宣告一個類型 T 是 NoRepeatTuple<T>,因為那是一個循環約束。但你可以使用一種類似於近似精確類型的技巧,通過製作一個只接受沒有重複元素的元組的函數來實現:
function requireNoRepeats<V extends "foo" | "bar" | "baz", T extends Array<V>>(
t: T & NoRepeatTuple<T>
) { }
參數 t 被宣告為 T & NoRepeatTuple<T>,將會導致編譯器推斷傳入 t 的型別為 T,然後使用 NoRepeatTuple<T> 進行檢查。讓我們看看是否可以嘗試一下:
const foo = "foo";
const bar = "bar";
const baz = "baz";
requireNoRepeats([foo, bar]) // error, must be tuple
哎呀!哦,問題是值 [foo, bar] 被推斷為陣列而不是元組。這是 TypeScript 的一個問題,我覺得它不會很快被修復。
所以我們必須說服編譯器將某些東西解釋為元組。我以前使用過的一種方法是編寫一個能推斷元組類型的函數。當 TS3.0 很快推出時,你可以使用元組的 rest/spread 簡潔地寫出來。
export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;
在那之前,你可以使用一個更詳細的函數。
好了,最後,讓我們試試看:
requireNoRepeats(tuple(foo, bar)) // okay
requireNoRepeats(tuple(foo, bar, baz)) // okay
requireNoRepeats(tuple(foo, bar, bar)) // error, no repeats please
requireNoRepeats(tuple(foo, foo)) // error, no repeats please
所以,這一切都能運作。耶!耶?如你所見,這裡有很多限制條件和類型操作的情況。至少對我而言,這不是我會在別人的程式碼中使用的東西,除非經過大量測試,對自己的程式碼倒是還可以。
無論如何,希望這能給你一些靈感。祝你好運!
