typescript - 無法找到某種類型的可變參數函數的正確類型定義
請看看以下這個小示範:
- 函數
f應該接受一對形式為 ['stringX', 'stringXstringX'] 的參數(-> 這個可以運作) - 但是函數
g應該接受這種模式的任意數量的對。我要如何輸入函數g才能實現這一點?
function f<T extends string, U extends `${T}${T}`>(arg: [T, U]) {}
function g(...args: ???[]) {} // what's the right type here (-> ???)?
f(['a', 'aa']) // ok
f(['a', 'ab']) // error as expected
g(
['a', 'aa'], // should be okay
['b', 'bb'], // should be okay
['c', 'ab'] // should result in a compile error!!!
)
» TypeScript 遊樂場
[編輯] 當然我可以將函數 g 修改為具有 n 個可選參數,並搭配 2*n 個型別參數,但這不是我正在尋找的解決方案。
[編輯] 順便說一下:如果函數 g 可以接受這些元組的一個陣列而不是可變參數,並且相應地運作,那也會是一個有用的解決方案,但我猜這是同樣的問題。
[編輯] 將 g 做成以下形式的單參數函數是可行的,但在語法上不是很好看:
// expected error at the 'ab'
g(['a', 'aa'])(['b', 'bb'])(['c', 'ab'])
觀看示範
答案#1
我在這裡的方法會是這樣:
function g<T extends string[]>(...args: { [I in keyof T]:
[T[I], NoInfer<Dupe<T[I]>>]
}) { }
type Dupe<T> = T extends string ? `${T}${T}` : never;
type NoInfer<T> = [T][T extends any ? 0 : never];
這個想法是 g() 在類型參數 T[] 上是泛型的,這是一個元組類型,僅由來自 args 中每對字串的第一個元素的字串字面量類型組成。所以如果你呼叫 g(["a", "aa"], ["b", "bb"], ["c", "cc"]),那麼 T 應該是元組類型 ["a", "b", "c"]。
args 的類型可以通過映射元組類型(mapped tuple type)以 T 表示。對於 T 元組中的每個數字索引 I,args 的對應元素應該具有類似 [T[I], Dupe<T[I]>] 的類型,其中 Dupe<T> 會以所需方式將字串與自身連接。因此,如果 T 是 ['z', 'y'],那麼 args 的類型應該是 [['z', 'zz'], ['y', 'yy']]。
這裡的目標是讓編譯器看到像 g(["x", "xx"], ["y", "oops"]) 這樣的調用,並從 args 的類型推斷出 T 為 ["x", "y"],然後檢查 args 的類型,並注意到雖然 ["x", "xx"] 是沒問題的,但 ["y", "oops"] 與預期的 ["y", "yy"] 不匹配,因此會出現錯誤。
這是基本的方法,但我們需要解決一些問題。
首先,在 microsoft/TypeScript#27995 存在一個未解決的問題,導致編譯器無法識別在映射元組類型 {[I in keyof T]: [T[I], Dupe<T[I]>]} 中,T[I] 一定會是 string。即使映射元組類型會創建另一個元組類型,編譯器仍認為 I 可能是 any[] 的其他鍵,例如 "map" 或 "length",因此 T[I] 可能是函數類型或 number 等。
所以如果我們定義了 Dupe<T> 就像
type Dupe<T extends string> = `${T}${T}`;
會有一個問題,即 Dupe<T[I]> 會導致錯誤。為了迴避這個問題,我已定義了 Dupe<T>,使其不需要 T 是 string。相反,它只是使用條件型別來表示「如果 T 是 string,那麼 Dupe<T> 將是 `$ {T}$ {T}`」:
type Dupe<T> = T extends string ? `${T}${T}` : never;
那就解決了那個問題。
下一個問題是,若要從類型為 {[I in keyof T]: [T[I], Dupe<T[I]>]} 的值推斷 T,編譯器可以同時查看每對 args 的第一個元素(T[I])以及第二個元素(Dupe<T[I]>)。該類型中每次提到 T[I] 都是 T[I] 的一個 推斷點。我們希望編譯器在推斷 T 時只查看每對的第一個元素,並僅對第二個元素進行 檢查。不幸的是,如果保持這樣,編譯器會試圖兩者都使用,導致困惑。
也就是說,當從 [["y", "zz"]] 推斷 T 時,編譯器會將 "y" 與 T[I] 相匹配,並將 "zz" 與 Dupe<T[I]> 相匹配,最終推斷出 T 的聯合類型 ["y" | "z"]。這其實相當聰明,因為如果 T 真的是 ["y" | "z"],那麼 args 的類型將會是 ["y" | "z", "yy" | "zz"],而值 ["y", "zz"] 正好符合它。太棒了!除了你想要 拒絕 [["y", "zz"]]。
所以我們需要某種方式告訴編譯器 不要 使用每對元素的第二個元素來推斷 T。在 microsoft/TypeScript#14829 上有一個未解決的問題,該問題要求對形式為 NoInfer<T> 的“非推斷性類型參數使用”提供一些支持。像 NoInfer<T> 這樣的類型最終會評估為 T,但會阻止編譯器嘗試從中推斷 T。因此我們會寫 {[I in keyof T]: [T[I], NoInfer<Dupe<T[I]>>]}。
在 TypeScript 中沒有內建或官方的 NoInfer<T> 類型,但有一些實現可以在某些情況下使用。我傾向於使用這個版本或類似的版本,以利用編譯器傾向於 延遲 評估未解析的條件類型的特性:
type NoInfer<T> = [T][T extends any ? 0 : never];
如果你為 T 指定某種特定類型,這將會被計算為 T(例如,NoInfer<string> 是 [string][string extends any ? 0 : never],也就是 [string][0],再簡化就是 string),但當 T 是未指定的泛型類型時,編譯器會保留它未計算,並不會急切地用 T 替換它。因此當 T 是某個泛型類型參數時,NoInfer<T> 對編譯器來說是不可見的,我們可以用它來阻止類型推論。
所以這就是解釋(呼!)。讓我們確保它有效:
g(
['a', 'aa'], // okay
['b', 'bb'], // okay
['c', 'dd'] // error!
// -> ~~~~
// Type '"dd"' is not assignable to type '"cc"'. (2322)
)
看起來不錯!編譯器對 ["a", "aa"] 和 ["b", "bb"] 很滿意,但對 ["c", "dd"] 卻會退避三舍,並給出一個很棒的錯誤訊息,說它原本期望 "cc",但實際得到的是 "dd"。
這回答了所提的問題。但請注意,對 g() 的類型定義實際上只對以 具體 和 字面 字串作為參數調用 g() 的 呼叫者 有用。當有人使用未解析的 泛型 字串(例如在某個泛型函數的函數體內)調用 g(),或者當你試圖在 g() 的實現體內檢查 T 或 args 時,你會發現編譯器實際上無法真正理解其含義。在那些情況下,你最好將類型擴展為類似 [string, string][] 之類的類型,並小心處理。
程式碼的 Playground 連結
