[英]Make a TypeScript type which represents the first letter of a string type
我有一個 function 輸出傳遞給它的字符串的第一個字母。 在我的例子中,我知道可能的值是什么,假設是硬編碼或通過泛型,並且希望函數的返回類型與返回的字母完全相同,因此我可以將其傳遞給以后的函數。
我實際上找到了一種相當不優雅的方式來做到這一點,但我感覺它不穩定並且可能無法在 TypeScript 的未來版本中工作,因為${infer FirstLetter}
在技術上可以代表任意數量的字符......它恰好是TypeScript目前只找到第一個:
type Speed = 'fast' | 'slow' | 'medium';
type SpeedShort = Speed extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
作為 function 聲明,這可能如下所示:
declare function firstLetter<Letters extends string>(
string: Letters,
): Letters extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
令人沮喪的是,類型'test'[0]
不是't'
,而是string
。 (同樣, 'test'['length']
不是4
,它是number
。)顯然,字符串文字類型在 Typescript 中不像元組類型那樣復雜( [0]
和['length']
都可以按預期工作對於['t', 'e', 's', 't']
)。 關於此限制有一個未解決的問題。
和你一樣,我帶着一些疑問查看`${infer H}${infer T}`
。 文檔中沒有任何內容表明它將始終完全匹配第一個H
和T
中的 rest 的一個字符。 然而, 原始拉取請求中有一個聲明准確描述了這種行為,它說:
通過從源中推斷出單個字符來匹配緊跟另一個占位符的占位符。
此外, 在另一條評論中,安德斯說:
通常,緊鄰的占位符實際上只對一次將字符串分開一個字符有用。
表明這是可以依賴的預期行為。 我想在文檔中找到它,但考慮到我們所擁有的, `${infer H}${string}`
可能是安全的,當然也是最好的可用情況,只要它是安全的。
但如果我們不使用它,仍然有選擇。 不是偉大的,但它們存在。 例如,對 Oleksandr 的答案的簡單改進是
function firstLetter<S extends Speed>(speed: S):
S extends 'fast' ? 'f' :
S extends 'medium' ? 'm' :
S extends 'slow' ? 's' :
never {
// implementation
}
這將准確返回您提供的任何Speed
(s) 的第一個字母。 所以firstLetter('fast')
不會有SpeedShort
的返回類型,它會有f
的類型。 並且(speed: 'fast' | 'slow') => firstLetter(speed)
的返回類型為'f' | 's'
'f' | 's'
。
更重要的是,這可能意味着更顯着的改進:
type FirstLetterOf<S extends string> =
string extends S ? string : // case where S is just string, not a literal type
S extends `a${string}` ? 'a' :
S extends `b${string}` ? 'b' :
S extends `c${string}` ? 'c' :
S extends `d${string}` ? 'd' :
S extends `e${string}` ? 'e' :
S extends `f${string}` ? 'f' :
S extends `g${string}` ? 'g' :
S extends `h${string}` ? 'h' :
S extends `i${string}` ? 'i' :
S extends `j${string}` ? 'j' :
S extends `l${string}` ? 'l' :
S extends `m${string}` ? 'm' :
S extends `n${string}` ? 'n' :
S extends `o${string}` ? 'o' :
S extends `p${string}` ? 'p' :
S extends `q${string}` ? 'q' :
S extends `r${string}` ? 'r' :
S extends `s${string}` ? 's' :
S extends `t${string}` ? 't' :
S extends `u${string}` ? 'u' :
S extends `v${string}` ? 'v' :
S extends `w${string}` ? 'w' :
S extends `x${string}` ? 'x' :
S extends `y${string}` ? 'y' :
S extends `z${string}` ? 'z' :
string;
function firstLetter<S extends string>(s: S): FirstLetterOf<S> { /*…*/ }
這將提取第一個字母......只要我們將“字母”定義為az
。 當然,您可以擴展它……但只能擴展到一定程度。 Typescript 的嵌套條件限制相當低,已經有 27 層深了。 添加大寫字母,您最多可以得到 53。
在這種情況下,嵌套條件問題的一個常見解決方案是使用映射類型,並遍歷它而不是嵌套所有可能性。 也就是說,這個:
type Letter =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
;
type FirstLetterOf<S extends string> =
string extends S ? string : // case where S is just string, not a literal type
{
[L in Letter]: S extends `${L}${string}` ? L : never;
}[Letter];
function firstLetter<S extends string>(s: S): FirstLetterOf<S> { /*…*/ }
現在,我們不再有嵌套條件,我們只有為每個Letter
運行一個條件。 現在添加字母相對容易,雖然我們可以添加的數量仍然有限制,但現在已經非常大了。
這仍然有一個問題:如果你輸入一個不以Letter
開頭的字符串文字,則轉彎類型是never
。 那是不對的; 它可能應該是string
(例如,我們的類型不夠聰明,無法縮小我們最終得到的字符串的范圍,但我們最終會得到一個)。 解決這個問題……看起來很討厭,但它仍然保持高性能並且不受嵌套限制的影響:
type Letter =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
;
type FirstLetterOf<S extends string> =
string extends S ? string : // case where S is just string, not a literal type
{
[L in Letter]: S extends `${L}${string}` ? L : never;
}[Letter] extends infer L
? [L, never] extends [never, L]
? string
: L
: never;
function firstLetter<S extends string>(s: S): FirstLetterOf<S> { /*…*/ }
extends infer L
業務有效地將類型的結果保存在類型L
中,然后[L, never] extends [never, L]
是一種檢查L
是否never
匹配的方法——也就是說,我們的Letter
都不匹配。 如果是這樣,我們的類型就是string
。 如果沒有,我們只是 go 和L
,因為這意味着Letter
匹配,這就是我們想要使用的。 最后一個: never
存在條件blah blah extends infer L
的“假情況”,這永遠不會是假的,因為我們infer L
是blah blah
實際上是什么,所以當然blah blah
擴展了它。 這里的語法……很奇怪,但它確實有效。 我們這里確實有幾層嵌套條件,但它是一個固定的數字,無論我們添加多少個Letter
都不會改變,所以這很好。
以下是您聲明此類類型的方法:
type Speed = 'fast' | 'slow' | 'medium'
type SpeedShort = 'f' | 's' | 'm'
這是您實現轉換功能的方法:
function firstLetter(speed: Speed): SpeedShort {
switch (speed) {
case 'fast': return 'f'
case 'slow': return 's'
case 'medium': return 'm'
}
}
我知道要編寫更多的代碼。 然而,這段代碼仍然表達了意圖,並由編譯器正確驗證,不會出現人為錯誤(即更改一種類型而不更改映射函數將導致編譯時錯誤)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.