You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// gives `true`, because `${string}${string}` type turns into string type but it shouldnt and should give `false`type_=stringextends `${string}${string}` ? true : false;// Similarly should be false because first ${string} gets the empty space only empty space, and there is nothing left to satify second ${string}type__=""extends `${string}${string}` ? true : false;
π Actual behavior
`${string}${string}` type turns into string type.
π Expected behavior
`${string}${string}` should stay as its.
Additional information about the issue
// gives `true`, because `${string}${string}` type turns into string type but it shouldnt and should give `false`type_=stringextends `${string}${string}` ? true : false;// Similarly should be false because first ${string} gets the empty space only empty space, and there is nothing left to satify second ${string}type__=""extends `${string}${string}` ? true : false;// What is correct{// works without error as expected because empty space at the start satisfies ${string}constfoo: `${string}@` ="@";// gives error as expected because first ${string} already uses the empty space at the start// and second one has no empty space to use// so before @ it expects at least one other characterconstbar: `${string}${string}@` ="@";// so this works as expectedconstbaz: `${string}${string}@` ="a@";}// What is wrong{// should give error but doesnt because `${string}${string}` type turns into string type// `${string}${string}` should stay as its and foo should expect its setter value to satisfy at least one characterconstfoo: `${string}${string}` ="";}
Not sure if its a feature or a bug, but and its not consistent, and it looks like a bug, sound like a bug, acts like a bug.
This has been explicitly changed in #41693 so it's intentional. I find the behavior slightly surprising though. This doesn't happen in an inference context but it feels like this (from the original PR that introduced template literal types should apply here):
A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
IIUC, this would make your interpretation correct.
A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.
Yes, but that also applies to e.g. `${string}${"bar" | "d" | "manchu"}` which is matched by "foobar", "food", and "foomanchu" but good luck extracting the foo part later using the same pattern. So the inconsistency there is hardly exclusive to `${string}${string}`. Joshua Chen (@Josh-Cena)'s reading of the situation seems more correct.
Joshua Chen (@Josh-Cena) ${string} is same as string, that's true, but ${string}${string} is not same as string.
Because and empty string can't mapped on to ${string}${string} only reason it does is ${string}${string} is wrongly being simplified into string.
If you:
`abc`extends`${inferA}${inferB}`
A is gonna get "a" and B is gonna get "bc".
Similarly if you:
`a`extends`${inferA}${inferB}`
A is gonna get "a" and B is gonna get "", if we also had C it wouldn't work because B already got the empty string "", so there is nothing left for the C.
Same thing for if the literal was empty string "" instead of "a", then A would get the empty string "", and there would be nothing left for the B nothing, not even an empty string.
which would expect at least one character before and after "-".
Why this happens?
So if we try to map "-" on CustomTag, it would look like this:
${string} : "-"
${string} : ""
"-" : not-matching because never doesn't satisfy "-"
${string} : never
${string} : never
So doesn't work.
But if we try to map "a-a" on CustomTag, it would look like this:
${string} : "a"
${string} : ""
"-" : "-"
${string} : "a"
${string} : ""
So it works.
Similarly if we try to map "foo-bar" on CustomTag, it would look like this:
${string} : "f"
${string} : "oo"
"-" : "-"
${string} : "b"
${string} : "ar"
Which again works.
So again, ${string} is same as string, that's true, but ${string}${string} is not same as string.
Only reason it acts like they are the same is because its hard-coded to be simplified into string, it seems based on the PR Mateusz BurzyΕski (@Andarist) shared.
typeExtractPrefix<T>=Textends `${infer U}${"bar"|"d"|"manchu"}` ? U : nevertypeFoo=ExtractPrefix<'food'|'foobar'|'foomanchu'>// gives "foo"
isn't this ${infer U}${"bar" | "d" | "manchu"} the same pattern?
Anyway, it doesn't matter, just realized the work around the weird behavior.
Just do ${infer A}${infer B} instead of ${string}${string}. It seems only ${string}${string} is hard-coded to be string.
Here:
type_=stringextends `${infer A}${infer B}` ? true : false;// gives false as it shouldtype_=""extends `${infer A}${infer B}` ? true : false;// again gives false as it should
DeepDoge A ${string} in a template literal acts like .* in a regex: it can consume from 0 to infinite characters. Therefore `${string}${string}` is able to match the empty string simply by both of them matching the empty string, because "" + "" === "".
You example with extends infer is simply because TS "tries to be helpful". You would likely not want any placeholder to match the empty string. However TS picking any substring would be legal behavior.
typeCustomTag= `${string}${string}-${string}${string}`
functionisCustomTag(value: string){return/.*.*-.*.*/.test(value)}constvalue="-"type_=typeofvalueextendsCustomTag ? true : false// Gives falseconsole.log(isCustomTag(value))// Returns trueconstsomeCustomTag: CustomTag=value// Type '"-"' is not assignable to type '`${string}${string}-${string}${string}`'
Also, infer is not supposed to change the type, its just suppose to extract part of a type. I can switch any type i want with an infer keyword and its not supposed to change the actual type.
Again, ${string} is equal to string that's true, but ${string}${string} is not equal to string. Only reason it acts like that, is, it's being simplified into string wrongly, while string is not simplified version of ${string}${string}, they are different.
As I showed in the example above, first ${string} gets the empty part, and there is never left for the second ${string}, so it doesn't match.
Is there precise documentation outlining exactly how inference and interpolations behave? I can't seem to find it. Looking around the web, I see some other similar problems where the solution is to use what appears to be equivalent with the explanation of "I don't know why this works but it does." E.g. ${any}${any} behaves differently from ${string}${string}
DeepDoge You're talking about the behavior of infer in a template literal, which is entirely separate from the concern of what's assignable to the type1. An empty string would still be assignable to `${string}${string}` even if it weren't simplified. Likewise, "foo123" is assignable to `${string}${number}`, but you won't be able to extract the "foo" part using that pattern later, because TS is hardcoded to only consume one character at a time for the first placeholder.
Tbh, I'm not even gonna try, ${any}${any} behaves correctly because they forgot to break it. So that's enough for me. ${string}${string} broken? broken things are everywhere. ${any}${any} works. Just avoid ${string} simple.
I don't care. It works, it works. Have a nice day everyone.
Conceptually, ${string}${string}should collapse to just string. ${string} includes an empty string, and two of those combined is still an empty string.
It's true that ${string}{infer U} yields never, but there's nothing in the documentation that states or implies that ${string}${string}, or even ${any}${any}, can't or shouldn't be simplified to string. If you rely on the latter you might be depending on unspecified (potentially subject to change) behavior.
This is why I was asking if there was any documentation on the specific behavior here. The typescript docs don't exactly read like a specification.
snarbies
never is 0, string is a valid type with the length field that has the number type that is between 0 and Infinity,
never & number = never because 0 * 123 = 0
never | number = number because 0 + 123 = 123
any | number = number because Infinity + 123 = Infinity
but that's not even the point, these are just analogies, and i was just mocking.
i dont maintain typescript, you can make it work any way you want.
i was showing that, expected behavior of template literals is:
A template string type has multiple types, separated with ${}
Like a for loop we go through every char that matches current type and get it as its part until:
current type satisfied without any fails
and until next part matches to the next type after current type is satisfied, if there is a next type.
if there is next type but no next part then it fails. since removing empty string from empty string is never.
and its not suppose to work differently than inferred version anyway.
this is how it works everywhere ${string}${string} alone or longer is just an exception.
im not complaining about adding two const numbers to each other gives you just number
or there is no clear way to differentiate between what type a mutable variable returns and what type it accepts while setting it.
or return type of a function is decided on based on its definition and not based on where you call it from with what arguments, so you have to repeat your runtime logic with types if its in a function.
or not being able define ranges like 0..255 & number or 0..255 & bigint
make ${string} act like .*.
even if there are no docs i can find, ${string}${string} behavior is the exception to the general rule of how it works currently everywhere. if this is a "bug" for you all. cool, break it.
As said, inferis supposed to work differently from substitutablility (i.e. type compatibility). And to put it in structural terms since TypeScript is structural...
And I was talking about ${string}${string} being an exception to how rest of the template literal type behaves.
As I showed here: #57233 (comment)
prefix${string}${string} or
${string}${string}suffix or
${string}${string}middle${string}${string} or
${any}${any} or
${infer A}${infer B},
etc.
it always means at least one character, except for ${string}${string} alone, that's the exception. ${string}${string} always means at least one character, except if its alone.
And I was talking about ${string}${string} being an exception to how rest of the template literal type behaves.
Ah, gotcha. You're right, that's definitely inconsistent. Unfortunately, that all appears to be unspecified behavior.
It seems like you're operating on the premise that if you start with an empty set and remove zero items, the remainder is never, and most of the behavior with tuples seems to validate that premise. To me that premise seems faulty on its face, but in practical terms the biggest issue is the inconsistency. If it could all go one way or the other, we could easily collectively shrug and move on with our lives.
Activity
Andarist commentedon Jan 30, 2024
This has been explicitly changed in #41693 so it's intentional. I find the behavior slightly surprising though. This doesn't happen in an inference context but it feels like this (from the original PR that introduced template literal types should apply here):
IIUC, this would make your interpretation correct.
Josh-Cena commentedon Jan 30, 2024
A
stringtype can match the empty string (like.*), so the two types are indeed equivalent. There is no type for "non-empty string".fatcerberus commentedon Jan 30, 2024
Mateusz BurzyΕski (@Andarist)
Yes, but that also applies to e.g.
`${string}${"bar" | "d" | "manchu"}`which is matched by"foobar","food", and"foomanchu"but good luck extracting thefoopart later using the same pattern. So the inconsistency there is hardly exclusive to`${string}${string}`. Joshua Chen (@Josh-Cena)'s reading of the situation seems more correct.nomadshiba commentedon Jan 30, 2024
Joshua Chen (@Josh-Cena)
${string}is same asstring, that's true, but${string}${string}is not same asstring.Because and empty string can't mapped on to
${string}${string}only reason it does is${string}${string}is wrongly being simplified intostring.If you:
A is gonna get "a" and B is gonna get "bc".
Similarly if you:
A is gonna get "a" and B is gonna get "", if we also had C it wouldn't work because B already got the empty string "", so there is nothing left for the C.
Same thing for if the literal was empty string "" instead of "a", then A would get the empty string "", and there would be nothing left for the B nothing, not even an empty string.
For example right now we can do types like this:
which would expect at least one character before and after "-".
Why this happens?
So if we try to map "-" on CustomTag, it would look like this:
neverdoesn't satisfy "-"So doesn't work.
But if we try to map "a-a" on CustomTag, it would look like this:
So it works.
Similarly if we try to map "foo-bar" on CustomTag, it would look like this:
Which again works.
So again,
${string}is same asstring, that's true, but${string}${string}is not same asstring.Only reason it acts like they are the same is because its hard-coded to be simplified into
string, it seems based on the PR Mateusz BurzyΕski (@Andarist) shared.Also, Bruce Pascoe (@fatcerberus) you can extract "foo", am I missing something?
isn't this
${infer U}${"bar" | "d" | "manchu"}the same pattern?Anyway, it doesn't matter, just realized the work around the weird behavior.
Just do
${infer A}${infer B}instead of${string}${string}. It seems only${string}${string}is hard-coded to bestring.Here:
Works perfectly.
Josh-Cena commentedon Jan 31, 2024
DeepDoge A
${string}in a template literal acts like.*in a regex: it can consume from 0 to infinite characters. Therefore`${string}${string}`is able to match the empty string simply by both of them matching the empty string, because"" + "" === "".You example with
extends inferis simply because TS "tries to be helpful". You would likely not want any placeholder to match the empty string. However TS picking any substring would be legal behavior.nomadshiba commentedon Feb 2, 2024
Joshua Chen (@Josh-Cena)
That's just wrong.
Playground
Also,

inferis not supposed to change the type, its just suppose to extract part of a type. I can switch any type i want with aninferkeyword and its not supposed to change the actual type.Again,
${string}is equal tostringthat's true, but${string}${string}is not equal tostring. Only reason it acts like that, is, it's being simplified intostringwrongly, whilestringis not simplified version of${string}${string}, they are different.As I showed in the example above, first
${string}gets the empty part, and there isneverleft for the second${string}, so it doesn't match.snarbies commentedon Feb 2, 2024
Is there precise documentation outlining exactly how inference and interpolations behave? I can't seem to find it. Looking around the web, I see some other similar problems where the solution is to use what appears to be equivalent with the explanation of "I don't know why this works but it does." E.g.
${any}${any}behaves differently from${string}${string}fatcerberus commentedon Feb 2, 2024
DeepDoge You're talking about the behavior of
inferin a template literal, which is entirely separate from the concern of what's assignable to the type1. An empty string would still be assignable to`${string}${string}`even if it weren't simplified. Likewise,"foo123"is assignable to`${string}${number}`, but you won't be able to extract the"foo"part using that pattern later, because TS is hardcoded to only consume one character at a time for the first placeholder.Footnotes
Assignability is the primary primitive in the type system and is generally the thing that determines which simplifications the compiler is allowed to do. This is how you get things like subtype reduction, etc. β©
nomadshiba commentedon Feb 2, 2024
Bruce Pascoe (@fatcerberus) I didn't only show examples with
infer. Again if you read examples i showed above and below:Playground
TypeScript uses the group theory, not regex.
Tbh, I'm not even gonna try,
${any}${any}behaves correctly because they forgot to break it. So that's enough for me.${string}${string}broken? broken things are everywhere.${any}${any}works. Just avoid${string}simple.I don't care. It works, it works. Have a nice day everyone.
Josh-Cena commentedon Feb 2, 2024
I think
`${string}${string}`not being equivalent to`${string}`in your cases above is a bug...nomadshiba commentedon Feb 2, 2024
Oh Really? 2 + 2 = 2, silly me. Didn't know.
snarbies commentedon Feb 2, 2024
This would be more like 0 + 0 = 0.
Conceptually,
${string}${string}should collapse to juststring.${string}includes an empty string, and two of those combined is still an empty string.It's true that
${string}{infer U}yieldsnever, but there's nothing in the documentation that states or implies that${string}${string}, or even${any}${any}, can't or shouldn't be simplified tostring. If you rely on the latter you might be depending on unspecified (potentially subject to change) behavior.This is why I was asking if there was any documentation on the specific behavior here. The typescript docs don't exactly read like a specification.
nomadshiba commentedon Feb 2, 2024
snarbies
never is 0, string is a valid type with the length field that has the number type that is between 0 and Infinity,
never & number = never because 0 * 123 = 0
never | number = number because 0 + 123 = 123
any | number = number because Infinity + 123 = Infinity
but that's not even the point, these are just analogies, and i was just mocking.
i dont maintain typescript, you can make it work any way you want.
i was showing that, expected behavior of template literals is:
A template string type has multiple types, separated with
${}Like a for loop we go through every char that matches current type and get it as its part until:
and its not suppose to work differently than inferred version anyway.
this is how it works everywhere
${string}${string}alone or longer is just an exception.number.*.even if there are no docs i can find,
${string}${string}behavior is the exception to the general rule of how it works currently everywhere. if this is a "bug" for you all. cool, break it.snarbies commentedon Feb 2, 2024
As said,
inferis supposed to work differently from substitutablility (i.e. type compatibility). And to put it in structural terms since TypeScript is structural...Playground link
So I'm not really clear on how you're interpreting this as an exception. Structurally,
${string}${string}is exactly equivalent tostring.nomadshiba commentedon Feb 2, 2024
snarbies
TypeScript doesn't create tuples types correctly already, simplifies it into an array.
Playground
And I was talking about
${string}${string}being an exception to how rest of the template literal type behaves.As I showed here: #57233 (comment)
prefix${string}${string}or${string}${string}suffixor${string}${string}middle${string}${string}or${any}${any}or${infer A}${infer B},it always means at least one character, except for
${string}${string}alone, that's the exception.${string}${string}always means at least one character, except if its alone.snarbies commentedon Feb 2, 2024
Ah, gotcha. You're right, that's definitely inconsistent. Unfortunately, that all appears to be unspecified behavior.
It seems like you're operating on the premise that if you start with an empty set and remove zero items, the remainder is
never, and most of the behavior with tuples seems to validate that premise. To me that premise seems faulty on its face, but in practical terms the biggest issue is the inconsistency. If it could all go one way or the other, we could easily collectively shrug and move on with our lives.typescript-bot commentedon Feb 5, 2024
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.