Template literal type inference failure due to lazy placeholder matching #49839
Description
Bug Report
🔎 Search Terms
template literal types, placeholders, pattern, inference, lazy/non-greedy
🕗 Version & Regression Information
- This is the behavior in every version I tried after template literal types were introduced in TS4.1, and I reviewed the FAQ for entries about template literal types
⏯ Playground Link
Playground link with relevant code
💻 Code
declare function f<T extends "x" | "y">(a: `${string}.${T}`): T;
const x = f("abc.x") // "x"
const y = f("def.y") // "y"
const z = f("ghi.jkl.x") // "x" | "y" !!!🙁 Actual behavior
In the third call, T is inferred as "x" | "y"
🙂 Expected behavior
T should be inferred as "x"
This came from this Stack Overflow question. It looks like the type `${string}.${T}` when compared to "ghi.jkl.x" lazily matches `${string}` as "ghi", so that it produces "jkl.x" as an inference candidate for T. This fails to match the "x" | "y" constraint. So that's an invalid inference candidate.
And I guess inference just fails, so T falls back to the constraint. Sure enough "ghi.jkl.x" does match `${string}.x` | `${string}.y`, so there's no error, but it's no longer useful as a generic call.
Just wondering where this falls on the spectrum from "bug" to "intentional".
(Note that this situation isn't quite the same as two immediately adjacent placeholders as in #46124 or #47048 or #49411.)
(Also note that my workaround for this would be to write a recursive conditional type to actually find the last delimiter, as shown here, but it's yucky.)
Activity
RyanCavanaugh commentedon Jul 11, 2022
This is pure speculation but I'm guessing what happens is:
"ghi.jkl.x"to${string}.${T}and collect a lone candidatejkl.x, since this is a greedy operationx | y, so we fall back to the constraint. Inference has effectively failed at this point; under most circumstances this would result in an error on the call, though strictly speaking this is absolutely not an error condition"ghi.jkl.x"against${string}.x | ${string}.yand succeed, since now we're using a back-anchored methodIt seems like in the first step we need to be applying the information about the constraint to the string-matcher-inference algorithm
jcalz commentedon Jul 11, 2022
I love the$\mathfrak{Cursed?}$ tag so much
RyanCavanaugh commentedon Jul 11, 2022
Anders Hejlsberg (@ahejlsberg) maybe you would enjoy looking at this if you want
Andarist commentedon Feb 1, 2023
I believe that the issue found by Andrea Simone Costa (@jfet97) is essentially the same as the one reported here (or at least - it fails due to the same implementation details):
It can be tested out in this TS playground.
What I've seen in the compiler when debugging this looks very close to what Ryan Cavanaugh (@RyanCavanaugh) has described here.
Could you elaborate on this one? I'm not sure if I understand this. I would assume that this should somehow be handled after the step 1.
RyanCavanaugh commentedon Feb 3, 2023
Right now the algorithm to infer to a template string doesn't "know" that it's trying to match against a type parameter with a constraint, so it doesn't realize that the inferences it's collecting are effectively possibly garbage. What we "should" do in this call
is do something effectively the same (but more efficient) as inferring to
"ghi.jkl.x"to${string}.x | ${string}.y, seeing which if any generated successful matches, and recording those as inference candidates (in this case, collecting a lone candidate"x"due to the successful match on the first one). That'd produce the expected result.Autumn-one commentedon Aug 18, 2023
The details of the template string implementation really confuse us in many scenarios, and maybe it needs time to improve