[英]Make a TypeScript type which represents the first letter of a string type
I have a function that outputs the first letter of the string passed into it.我有一个 function 输出传递给它的字符串的第一个字母。 In my case I know what the possible values are, assume either hard-coded or through a generic, and want the function's return type to be exactly what the letter being returned is, so I can pass this on to later functions.
在我的例子中,我知道可能的值是什么,假设是硬编码或通过泛型,并且希望函数的返回类型与返回的字母完全相同,因此我可以将其传递给以后的函数。
I have actually found a rather inelegant way to do it, but I have a feeling that it's not stable and may not work in future versions of TypeScript, as ${infer FirstLetter}
could technically represent any number of characters… it just so happens that TypeScript currently only finds the first one:我实际上找到了一种相当不优雅的方式来做到这一点,但我感觉它不稳定并且可能无法在 TypeScript 的未来版本中工作,因为
${infer FirstLetter}
在技术上可以代表任意数量的字符......它恰好是TypeScript目前只找到第一个:
type Speed = 'fast' | 'slow' | 'medium';
type SpeedShort = Speed extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
As a function declaration this may look like:作为 function 声明,这可能如下所示:
declare function firstLetter<Letters extends string>(
string: Letters,
): Letters extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
Frustratingly, the type 'test'[0]
isn't 't'
, it's string
.令人沮丧的是,类型
'test'[0]
不是't'
,而是string
。 (Likewise, 'test'['length']
isn't 4
, it's number
.) Clearly, string literal types are not as sophisticated in Typescript as tuple types are (both [0]
and ['length']
would work as expected for ['t', 'e', 's', 't']
). (同样,
'test'['length']
不是4
,它是number
。)显然,字符串文字类型在 Typescript 中不像元组类型那样复杂( [0]
和['length']
都可以按预期工作对于['t', 'e', 's', 't']
)。 There is an open issue about this limitation .关于此限制有一个未解决的问题。
Like you, I viewed `${infer H}${infer T}`
with some doubt.和你一样,我带着一些疑问查看
`${infer H}${infer T}`
。 There's nothing in the docs that says it will always match exactly one character for the first H
and the rest in T
. 文档中没有任何内容表明它将始终完全匹配第一个
H
和T
中的 rest 的一个字符。 There is, however, a statement in the original pull request that describes exactly this behavior , saying:然而, 原始拉取请求中有一个声明准确描述了这种行为,它说:
A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
通过从源中推断出单个字符来匹配紧跟另一个占位符的占位符。
Also, in another comment, Anders says :此外, 在另一条评论中,安德斯说:
In general, immediately adjacent placeholders are really only useful for taking strings apart one character at a time.
通常,紧邻的占位符实际上只对一次将字符串分开一个字符有用。
suggesting that this is intended behavior that can be relied upon.表明这是可以依赖的预期行为。 I would like to find it in the docs, but given what we have,
`${infer H}${string}`
is probably safe, and certainly the best available situation so long as it is safe.我想在文档中找到它,但考虑到我们所拥有的,
`${infer H}${string}`
可能是安全的,当然也是最好的可用情况,只要它是安全的。
But if we don't use that, there are still options.但如果我们不使用它,仍然有选择。 Not great ones, but they exist.
不是伟大的,但它们存在。 For instance, an easy improvement on Oleksandr's answer would be
例如,对 Oleksandr 的答案的简单改进是
function firstLetter<S extends Speed>(speed: S):
S extends 'fast' ? 'f' :
S extends 'medium' ? 'm' :
S extends 'slow' ? 's' :
never {
// implementation
}
This will return precisely the first letter of whatever Speed
(s) you give.这将准确返回您提供的任何
Speed
(s) 的第一个字母。 So firstLetter('fast')
won't have a return type of SpeedShort
, it'll have a type of f
.所以
firstLetter('fast')
不会有SpeedShort
的返回类型,它会有f
的类型。 And (speed: 'fast' | 'slow') => firstLetter(speed)
will have a return type of 'f' | 's'
并且
(speed: 'fast' | 'slow') => firstLetter(speed)
的返回类型为'f' | 's'
'f' | 's'
. 'f' | 's'
。
More importantly, this perhaps suggests a more significant improvement:更重要的是,这可能意味着更显着的改进:
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> { /*…*/ }
This will extract the first letter... as long as we define “letter” as az
.这将提取第一个字母......只要我们将“字母”定义为
az
。 You can, of course, extend it... but only up to a point.当然,您可以扩展它……但只能扩展到一定程度。 Typescript has a rather low nested condition limit, and that's already 27 layers deep.
Typescript 的嵌套条件限制相当低,已经有 27 层深了。 Add uppercase letters and you're up to 53.
添加大写字母,您最多可以得到 53。
A common solution to nested condition issues in situations like these is to use a mapping type, and walk through that rather than nesting all the possibilities.在这种情况下,嵌套条件问题的一个常见解决方案是使用映射类型,并遍历它而不是嵌套所有可能性。 That is, this:
也就是说,这个:
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> { /*…*/ }
Now, we no longer have nested conditions, we just have one condition that we run for each Letter
.现在,我们不再有嵌套条件,我们只有为每个
Letter
运行一个条件。 Now it's relatively easy to add letters, and while there's still a limit on how many we can add, it's now very large.现在添加字母相对容易,虽然我们可以添加的数量仍然有限制,但现在已经非常大了。
This still has a problem: if you put in a string literal that doesn't start with a Letter
, the turn type is never
.这仍然有一个问题:如果你输入一个不以
Letter
开头的字符串文字,则转弯类型是never
。 That's not right;那是不对的; it should probably be
string
(as in, our type isn't smart enough to narrow down which string we'll end up with but we're going to end up with one).它可能应该是
string
(例如,我们的类型不够聪明,无法缩小我们最终得到的字符串的范围,但我们最终会得到一个)。 Fixing that... looks nasty, but it remains performant and safe from nesting limits:解决这个问题……看起来很讨厌,但它仍然保持高性能并且不受嵌套限制的影响:
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> { /*…*/ }
The extends infer L
business effectively saves the result of the type in type L
, and then [L, never] extends [never, L]
is a way to check if L
is never
—that is, none of our Letter
s matched. extends infer L
业务有效地将类型的结果保存在类型L
中,然后[L, never] extends [never, L]
是一种检查L
是否never
匹配的方法——也就是说,我们的Letter
都不匹配。 If so, our type is string
instead.如果是这样,我们的类型就是
string
。 If not, we just go with L
, since that means a Letter
matched and that's what we want to use.如果没有,我们只是 go 和
L
,因为这意味着Letter
匹配,这就是我们想要使用的。 The last : never
is there as the “false case” for the condition blah blah extends infer L
, which can't ever be false since we infer L
to be whatever blah blah
actually is, so of course blah blah
extends it.最后一个
: never
存在条件blah blah extends infer L
的“假情况”,这永远不会是假的,因为我们infer L
是blah blah
实际上是什么,所以当然blah blah
扩展了它。 The syntax here is... weird, but it works.这里的语法……很奇怪,但它确实有效。 We do have a few layers of nested conditions here, but it's a fixed number that doesn't change no matter how many
Letter
s we add, so that's fine.我们这里确实有几层嵌套条件,但它是一个固定的数字,无论我们添加多少个
Letter
都不会改变,所以这很好。
Here's how you declare such type:以下是您声明此类类型的方法:
type Speed = 'fast' | 'slow' | 'medium'
type SpeedShort = 'f' | 's' | 'm'
And here's how you implement conversion function:这是您实现转换功能的方法:
function firstLetter(speed: Speed): SpeedShort {
switch (speed) {
case 'fast': return 'f'
case 'slow': return 's'
case 'medium': return 'm'
}
}
I know that it is more code to write.我知道要编写更多的代码。 However, this code still expresses the intent and is validated by a compiler correctly leaving no place for human error (ie changing one type without changing the mapping function will result in compile-time error).
然而,这段代码仍然表达了意图,并由编译器正确验证,不会出现人为错误(即更改一种类型而不更改映射函数将导致编译时错误)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.