Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to infer a substring in a template literal leads to the type not matching the template #55855

Closed
JesusTheHun opened this issue Sep 25, 2023 · 8 comments
Labels
Duplicate An existing issue was already created

Comments

@JesusTheHun
Copy link

πŸ”Ž Search Terms

template literal infer, literal infer, infer match

πŸ•— Version & Regression Information

This changed between versions 4.6.4 and 4.7.4. Doesn't no work in 5.2.2 nor nightly.

⏯ Playground Link

https://tsplay.dev/WzDbrN

πŸ’» Code

type InferDelimiter<
  T extends string,
  Delimiter extends string,
> = T extends `${infer Head}${infer Mark extends Delimiter}${string}` ? true : false;

type T1 = InferDelimiter<'foo.bar', '.'>;

πŸ™ Actual behavior

T1 is evaluated to false.

πŸ™‚ Expected behavior

T1 is evaluated to true.

Additional information about the issue

No response

@MartinJohns
Copy link
Contributor

Your playground link does not match the code from your issue.

Your inferred type ${infer Mark extends Delimiter}${string} will first infer the string, then check if it extends Delimiter. That's not going to work well.

Any reason why you're inferring the delimiter when you get it provided? When you write ${infer Head}${Delimiter}${string} it will work.

@JesusTheHun
Copy link
Author

JesusTheHun commented Sep 25, 2023

Your playground link does not match the code from your issue.

The code in the issue only illustrate the issue itself.
The code in the playground include variants of the code when it behaves as expected, to help maintainers narrow down the issue.

Your inferred type ${infer Mark extends Delimiter}${string} will first infer the string, then check if it extends Delimiter. That's not going to work well.

I'm not sure what you mean by that. Are you saying it will try to match the string starting by the end up until it finds Delimiter ?

Any reason why you're inferring the delimiter when you get it provided? When you write ${infer Head}${Delimiter}${string} it will work.

I was writing a type with Delimiter being a union, I didn't get what I wanted so I tried to break it down to figure out the issue. So yeah, in my use case Delimiter will be a union.

@Andarist
Copy link
Contributor

Andarist commented Sep 25, 2023

I think this is likely related to #49839 (it might not be the same though)

Just like @MartinJohns said, the problem here is that inferring into type variables with extends doesn't take those constraints into account when inferring - those constraints are only checked later. So what really happens here is something close to this:

type InferDelimiter<
  T extends string,
  Delimiter extends string,
> = T extends `${infer Head}${infer Mark}${string}`
  ? Mark extends Delimiter
    ? true
    : false
  : false;

type T1 = InferDelimiter<"foo.bar", ".">;

I actually find this behavior quite confusing and I think it would be better if those constraints were taken into account while inferring to template literal types. They are sometimes taken into account when inferring into tuple elements - the constraint infers the algorithm how many fixed elements can be "consumed" from the source. It would make sense to do something similar here. It might be tricky to do it in an efficient way though - checking against the constraint after each consumed character doesn't sound like a good idea.

@JesusTheHun
Copy link
Author

JesusTheHun commented Sep 25, 2023

Oh ok, thanks for the explanation, I understand now.
This behaviour is indeed confusing.

As for the performance of taking the constraints into account while inferring, because it is inside a template literal, several dramatic optimisations can be made to turn this into a O(1).

Edit : Regarding this issue vs #49839 , I'm unsure because this issue shows up with version 4.7, while #49839 seems to have always been there.

@fatcerberus
Copy link

fatcerberus commented Sep 25, 2023

@Andarist If I recall from past discussions, it works this way intentionally for performance reasons, so the template matching algorithm doesn’t ever have to backtrack (any given placeholder either matches on the first try, or it doesn’t match at all). Two directly adjacent placeholders will only ever match a single character at the first one, so you can do stuff like ${infer H}${infer T} recursively to extract characters from the string one at a time. This also, however, means that ${number}${string} doesn’t work intuitively either, and I believe there are issues about that that have been closed as by design/design limitation. People who want anything more complex tend to get redirected to #6579.

@Andarist
Copy link
Contributor

I think there might be a way to implement this with simpler lookaheads instead of backtracking - but since the added constraint might be a union (or a generic, or a non-literal...) it definitely makes the whole thing quite tricky 😒 I was already thinking about this one a couple of times and each time... I gave up πŸ˜…

@JesusTheHun
Copy link
Author

since the added constraint might be a union (or a generic, or a non-literal...) it definitely makes the whole thing quite tricky

Out of curiosity, what makes it more tricky ? Union means combinatory so it's the same process repeated for each member, for each union, right ? What about generics ?

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Sep 25, 2023
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Sep 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants