[英]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.