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?
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.