简体   繁体   English

创建一个 TypeScript 类型,代表字符串类型的第一个字母

[英]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 . 文档中没有任何内容表明它将始终完全匹配第一个HT中的 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
}

See in Playground 在操场上看

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> { /*…*/ }

See in Playground 在操场上看

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> { /*…*/ }

See in Playground 在操场上看

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> { /*…*/ }

See in Playground 在操场上看

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 Lblah 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'
    }
}

playground 操场

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.

相关问题 什么类型代表打字稿中的类型? - What type represents a type in typescript? Typescript:如何声明代表Treeview的Type? 可能吗? - Typescript: How to declare a Type which represents a Treeview? Is it possible? 如何在Typescript中指定表示2个函数的并集的类型 - How to specify a type in Typescript which represents the union of 2 functions 如何在 TypeScript 中编写表示元组类型的接口? - How to write an interface represents a tuple type in TypeScript? 是否有一种类型表示具有任何属性但不是数组的对象 - Is there a type which represents an object with any property but is no array 是否可以在打字稿中创建一个类型来表示除一组字符串文字之外的所有字符串? - Is it possible to create a type in typescript that represents all strings except for a set of string literals? Typescript 使第二个参数类型依赖于第一个参数类型 - Typescript make the second parameter type depends on the first parameter type 我可以在 Typescript 中创建一个计算字符串类型吗? - Can I make a computed string type in Typescript? 如果泛型的参数可以是字符串或数字,如何让 TypeScript 知道将返回哪种类型 - How to make TypeScript know which type will be returned if generic's parameter could be string OR number TypeScript 如何将字符串联合类型转换为具体的对象类型? - TypeScript How to make String Union Type to a concrete Obnject Type?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM