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:
- Template literal type
Sis a subtype of template literal typeTif a string that would matchSwould also matchT.
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
stringas in`abc${string}ghi`or with indeterminate types likeinfer Uas inT 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 Ywhen every value of typeXcan be safely assigned to a variable of typeY. 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}`:But template literals consisting only of repeated
stringplaceholders are collapsed tostring, so""does match`${string}${string}`. See microsoft/TypeScript#57355.And a
numberplaceholder doesn't refer to "those strings which are what you get when you serialize anumberwith a template literal string"; instead it refers to "anything which can be successfully parsed as anumber", which also leads to weird behavior. See microsoft/TypeScript#57404 and this comment on a related issue microsoft/TypeScript#41893:So while TypeScript does more or less follow the rule that "if
Xis assignable toYthenX extends Y ? true : falseistrue, sometimes it's not obvious whenXis assignable toY.Playground link to code