Search code examples
typescript

How to replace `${number}` type to just string type '${number}' in TypeScript?


Let's say I have type Foo:

type Foo = 'apple' | `a.${number}` | `a.${number}.c`

Eventually I want to convert it to Boo:

type Boo = ReplaceTemplateWithLiteral<Foo>
// 'apple' | 'a.${number}' | 'a.${number}.c'

I know that I can extract string type that includes ${number} by:

Extract<Foo, `${string}.${number}` | `${string}.${number}.${string}`>
// `a.${number}` | `a.${number}.c`

But I don't know how to convert it to 'a.${number}' | 'a.${number}.c' which is just string literals.

Is this even possible?


Solution

  • Note: I'm only interested in supporting replacing the `${number}` pattern/placeholder template literal type (see microsoft/TypeScript#40598) with the string literal type "${number}". I'm not going to worry about replacing other placholder template literal types, like a generic one of the form `${T}` (where T is a generic type parameter), or `${string}`. If you have one of those in your template literal type then all bets are off.

    You could do it like this:

    type ReplaceNumberWithLiteral<T extends string, A extends string = ""> =
      T extends `${infer F}${infer R}` ?
      ReplaceNumberWithLiteral<R, `${A}${`${number}` extends F ? "${number}" : F}`> : 
      A;
    

    Here we're using a tail-recursive conditional type to walking through the string literal character by character, and if it finds `${number}` then it replaces it with "${number}", and otherwise it leaves what it finds alone. Note that detecting `${number}` is a little tricky; you can't check F extends `${number}` because if F is a numeric character like "3" then it will succeed, and you don't want to turn "ABC123" into "ABC${number}${number}${number}". I've changed it to `${number}` extends F which works for the use cases we want to support. Of course if F is `${string}` then it would succeed, and so something like `a${string}b` will probably turn into "a${number}b". If that's important, then one could try to change the above code, but it's out of scope for the question as asked.

    Let's make sure it works:

    type Foo = 'apple' | `a.${number}` | `a.${number}.c`
    type Bar = ReplaceNumberWithLiteral<Foo>
    //   ^? type Bar = "apple" | "a.${number}" | "a.${number}.c"
    

    Looks good.

    Playground link to code