簡體   English   中英

創建一個 TypeScript 類型,代表字符串類型的第一個字母

[英]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}` 文檔中沒有任何內容表明它將始終完全匹配第一個HT中的 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 Lblah 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM