繁体   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