What is the subtype rule for template literal types with placeholders (like `${number}em`
)? It's not covered by the Template Literal Types part of the handbook and I didn't find anything for it in searches though I could have missed it somewhere; it's tricky to search for.
I ask because I was surprised recently by `${number}` extends `${string}`
being true. On reflection, I have a reasonable thought for why it is, but I'm looking either for confirmation and details if I'm right or the correct explanation if I'm wrong. (For context, this came up while I was trying to prove to myself that my answer to this question was reliable.)
My thought is:
S
is a subtype of template literal type T
if a string that would match S
would also match T
.That's true for my `${number}` extends `${string}`
example because any series of characters that would match `${number}`
(like "123"
) would also match `${string}`
.
That also seems like a reasonable fit for the general rule that "...any term of type S
can safely be used in any context where a term of type T
is expected...".
Is that correct and if so, is there further nuance? If not, what is the correct definition?
FWIW, here is my somewhat-meandering series of tests that I used to test my intuition:
type A = `${number}` extends `${number}` ? true : false;
// ^?
// true, since they're the same thing, so yeah, it's a match
type B = `${number}` extends `${number|string}` ? true : false;
// ^?
// true, anything matching `${number}` would also match `${number|string}`
type C = `${number}` extends `${string}` ? true : false;
// ^?
// true, which makes sense for a template literal even though number isn't a subtype of
// string, because anything matching `${number}` would also match `${string}` --
// that is, a series of digits is not just a valid number, but also a valid string
type D = `${string}` extends `${number}` ? true : false;
// ^?
// false, because the converse isn't true; not all sequences of characters are
// valid numbers
type E = `${number|string}` extends `${number}` ? true : false;
// ^?
// false - same as D, basically
type F = `${number}` extends `${string}${number}` ? true : false;
// ^?
// false because something matching `${number}` wouldn't match `${string}${number}`
type G = `${string}${number}` extends `${string}${number}` ? true : false;
// ^?
// true, they're the same thing
type H = `${number}${string}` extends `${string}${number}` ? true : false;
// ^?
// false, for the same reason as D and E
type I = `${number}x` extends `${string}x` ? true : false;
// ^?
// true - basically the same as C, just with an x on it
type J = `${number}x` extends `${string}y` ? true : false;
// ^?
// false because something matching `${number}x` wouldn't match `${string}y`
Assignability of template literal types with placeholders (with wide types like string
as in `abc${string}ghi`
or with indeterminate types like infer U
as in T extends `abc${infer U}ghi` ? U : never
) was implemented in microsoft/TypeScript#43361. So that's the authoritative answer for how this works.
Conceptually, yes, X extends Y
when every value of type X
can be safely assigned to a variable of type Y
. So your intuition is correct and explains most of what's going on.
Except that template literal types with placeholders don't always behave as some people expect.
For example, when two placeholders are next to each other, the first one matches exactly one character from the input. So "a"
doesn't match `a${string}${string}`
:
type Hmm = "a" extends `a${string}${string}` ? true : false
// ^? type Hmm = false
But template literals consisting only of repeated string
placeholders are collapsed to string
, so ""
does match `${string}${string}`
. See microsoft/TypeScript#57355.
type Wha = "" extends `${string}${string}` ? true : false
// ^? type Wha = true
And a number
placeholder doesn't refer to "those strings which are what you get when you serialize a number
with a template literal string"; instead it refers to "anything which can be successfully parsed as a number
", which also leads to weird behavior. See microsoft/TypeScript#57404 and this comment on a related issue microsoft/TypeScript#41893:
type Oops = "a 0" extends `a${number}` ? true : false
// ^? type Oops = true
So while TypeScript does more or less follow the rule that "if X
is assignable to Y
then X extends Y ? true : false
is true
, sometimes it's not obvious when X
is assignable to Y
.