创建一个表示字符串类型首字母的 TypeScript 类型
Make a TypeScript type which represents the first letter of a string type
我有一个函数可以输出传递给它的字符串的第一个字母。在我的例子中,我知道可能的值是什么,假设是硬编码或通过泛型,并且希望函数的 return 类型与被 returned 的字母完全相同,所以我可以通过这将用于以后的功能。
我实际上找到了一种相当不优雅的方式来做到这一点,但我觉得它不稳定并且可能无法在未来版本的 TypeScript 中工作,因为 ${infer FirstLetter}
在技术上可以表示任意数量的字符......碰巧 TypeScript 目前只找到第一个:
type Speed = 'fast' | 'slow' | 'medium';
type SpeedShort = Speed extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
作为函数声明,它可能看起来像:
declare function firstLetter<Letters extends string>(
string: Letters,
): Letters extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
声明这种类型的方法如下:
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'
}
}
我知道写的代码比较多。
然而,这段代码仍然表达了意图,并由编译器正确验证,没有人为错误的余地(即改变一种类型而不改变映射函数将导致编译时错误)。
令人沮丧的是,类型 'test'[0]
不是 't'
,而是 string
。 (同样,'test'['length']
不是 4
,而是 number
。)显然,字符串文字类型在 Typescript 中并不像元组类型那样复杂([0]
和 ['length']
将按预期工作 ['t', 'e', 's', 't']
)。有an open issue about this limitation.
和您一样,我对 `${infer H}${infer T}`
也有些怀疑。 the docs that says it will always match exactly one character for the first H
and the rest in T
. There is, however, a statement in the original pull request that describes exactly this behavior里面什么都没有说:
A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
此外,in another comment, Anders says:
In general, immediately adjacent placeholders are really only useful for taking strings apart one character at a time.
表明这是可以信赖的预期行为。我想在文档中找到它,但考虑到我们所拥有的,`${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
}
这将 return 恰好是您提供的任何 Speed
(s) 的第一个字母。所以 firstLetter('fast')
不会有 return 类型的 SpeedShort
,它会有 f
类型。 (speed: 'fast' | 'slow') => firstLetter(speed)
将具有 return 类型的 '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> { /*…*/ }
这将提取第一个字母...只要我们将“字母”定义为a-z
。当然,您可以扩展它……但只能扩展到一定程度。 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
(例如,我们的类型不够聪明,无法缩小 which 字符串的范围,但我们最终会得到一)。解决这个问题……看起来很糟糕,但它仍然保持高性能并且不受嵌套限制的影响:
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
是否为[=36=的方法]——也就是说,我们 Letter
中的 none 匹配。如果是这样,我们的类型就是 string
。如果没有,我们就使用 L
,因为这意味着 Letter
匹配,这就是我们想要使用的。最后一个 : never
是条件 blah blah extends infer L
的“假情况”,它永远不会是假的,因为我们 infer L
是 blah blah
实际上是什么,所以当然 blah blah
扩展了它。这里的语法……很奇怪,但它确实有效。我们这里确实有几层嵌套条件,但是它是一个固定的数字,无论我们添加多少Letter
都不会改变,所以没关系。
我有一个函数可以输出传递给它的字符串的第一个字母。在我的例子中,我知道可能的值是什么,假设是硬编码或通过泛型,并且希望函数的 return 类型与被 returned 的字母完全相同,所以我可以通过这将用于以后的功能。
我实际上找到了一种相当不优雅的方式来做到这一点,但我觉得它不稳定并且可能无法在未来版本的 TypeScript 中工作,因为 ${infer FirstLetter}
在技术上可以表示任意数量的字符......碰巧 TypeScript 目前只找到第一个:
type Speed = 'fast' | 'slow' | 'medium';
type SpeedShort = Speed extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
作为函数声明,它可能看起来像:
declare function firstLetter<Letters extends string>(
string: Letters,
): Letters extends `${infer FirstLetter}${string}`
? FirstLetter
: never;
声明这种类型的方法如下:
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'
}
}
我知道写的代码比较多。 然而,这段代码仍然表达了意图,并由编译器正确验证,没有人为错误的余地(即改变一种类型而不改变映射函数将导致编译时错误)。
令人沮丧的是,类型 'test'[0]
不是 't'
,而是 string
。 (同样,'test'['length']
不是 4
,而是 number
。)显然,字符串文字类型在 Typescript 中并不像元组类型那样复杂([0]
和 ['length']
将按预期工作 ['t', 'e', 's', 't']
)。有an open issue about this limitation.
和您一样,我对 `${infer H}${infer T}`
也有些怀疑。 the docs that says it will always match exactly one character for the first H
and the rest in T
. There is, however, a statement in the original pull request that describes exactly this behavior里面什么都没有说:
A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
此外,in another comment, Anders says:
In general, immediately adjacent placeholders are really only useful for taking strings apart one character at a time.
表明这是可以信赖的预期行为。我想在文档中找到它,但考虑到我们所拥有的,`${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
}
这将 return 恰好是您提供的任何 Speed
(s) 的第一个字母。所以 firstLetter('fast')
不会有 return 类型的 SpeedShort
,它会有 f
类型。 (speed: 'fast' | 'slow') => firstLetter(speed)
将具有 return 类型的 '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> { /*…*/ }
这将提取第一个字母...只要我们将“字母”定义为a-z
。当然,您可以扩展它……但只能扩展到一定程度。 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
(例如,我们的类型不够聪明,无法缩小 which 字符串的范围,但我们最终会得到一)。解决这个问题……看起来很糟糕,但它仍然保持高性能并且不受嵌套限制的影响:
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
是否为[=36=的方法]——也就是说,我们 Letter
中的 none 匹配。如果是这样,我们的类型就是 string
。如果没有,我们就使用 L
,因为这意味着 Letter
匹配,这就是我们想要使用的。最后一个 : never
是条件 blah blah extends infer L
的“假情况”,它永远不会是假的,因为我们 infer L
是 blah blah
实际上是什么,所以当然 blah blah
扩展了它。这里的语法……很奇怪,但它确实有效。我们这里确实有几层嵌套条件,但是它是一个固定的数字,无论我们添加多少Letter
都不会改变,所以没关系。