Repository navigation
Problem with infer two types from string at same time #56936
Copy link
Copy link
Open
Labels
Needs InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
Milestone
Description
🔎 Search Terms
If i want infer two types from string in same time with infer U extends Subtype, I get different results depending on the input string
type Test<T extends string> = T extends `${infer Rest}${infer D extends Digit}`
? `${Rest}[${D}]`
: TIt can be solved with two consecutive conditions, but it has its price ( in example you can see three different implementations with different result)
🕗 Version & Regression Information
- This code does not work in versions earlier than 4.7
- This changed between versions 4.7 and 4.8
- The result given in the comments to the code corresponds to versions 4.8-5.3
⏯ Playground Link
💻 Code
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
// infer last digit & rest of string in one condition
type Test_1<T extends `${number}`> = T extends `${infer Rest}${infer D extends Digit}`
? [Rest, D]
: never // <T extends `${number}>` protected us before
// infer rest of string, then infer last digit second condition
type Test_2<T extends `${number}`> = T extends `${infer Rest}${Digit}`
? T extends `${Rest}${infer D extends Digit}`
? [Rest, D]
: never
: never // <T extends `${number}>` protected us before
// infer last digit, then infer rest of string second condition
type Test_3<T extends `${number}`> = T extends `${string}${infer D extends Digit}`
? T extends `${infer Rest}${D}`
? [Rest, D]
: never
: never // <T extends `${number}>` protected us before🙁 Actual behavior
type test_11 = Test_1<'101'> // unexpected: never
type test_12 = Test_1<'10'> // ok: ['1', 0]
type test_13 = Test_1<'1'> // unexpected: never
type test_14 = Test_1<'0'> // unexpected: never
//@ts-expect-error
type test_15 = Test_1<''> // ok
type test_21 = Test_2<'101'> // ok: ['10', 1]
type test_22 = Test_2<'10'> // ok: ['1', 0]
type test_23 = Test_2<'1'> // ok: ['', 1]
type test_24 = Test_2<'0'> // ok: ['', 0]
//@ts-expect-error
type test_25 = Test_2<''> // ok
type test_31 = Test_3<'101'> // unexpected: ['10', Digit]
type test_32 = Test_3<'10'> // ok: ['1', 0]
type test_33 = Test_3<'1'> // unexpected: ['', Digit]
type test_34 = Test_3<'0'> // unexpected: ['', Digit]
//@ts-expect-error
type test_35 = Test_3<''> // ok🙂 Expected behavior
Test_1, Test_2 & Test_3 has equal behaviour
Additional information about the issue
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Needs InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
Activity
fatcerberus commentedon Jan 3, 2024
The way template string inference works, consecutive placeholders can only have a head/tail relationship. You can’t do it in reverse -
infer Restwon’t consume more than one character, as doing so would require backtracking.AtataUruru commentedon Jan 3, 2024
This would be closer to the truth if TS prohibited the use of anything other than "string" in the suffix. And since TS understands "SuffixType", then I expect that it will be able to inference it
${infer Rest}${SuffixType}Besides, this works as expected:
fatcerberus commentedon Jan 3, 2024
It is the truth. It might not meet your needs, but that's intentionally how the algorithm works. Each placeholder only gets one chance to match, and the first match for each (going from left to right) is locked in. Whichever ones are left can only work with what remains of the string.
So when you do
Test_1<'101'>what happens is this:${infer Rest}is immediately followed by another placeholder, so always match at least one character.Rest = 1? Yes, it's unconstrained so that matches; take it and lock it in.${infer D extends Digit}D = 0? Yes, it matches andextends Digit, so lock it in.Yes, because
Digitis always one character, so the first match for the non-trailing elements always succeeds. You can't do a variable-sized match in the middle of a string unless it's anchored to a fixed character.AtataUruru commentedon Jan 3, 2024
if it work without
infer T extends, it have to work with it the same wayThe root of the problem is that these two methods give different results in certain cases:
Yes, I saw the remark in documentation.
But this does not explain the difference in behavior
For example, I collected combinations of checking a string and inference a substring:
Playground
typescript-bot commentedon Jan 6, 2024
This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
AtataUruru commentedon Jan 6, 2024
the problem was not resolved
AtataUruru commentedon Jan 8, 2024
Ryan Cavanaugh (@RyanCavanaugh) up
RyanCavanaugh commentedon Jan 8, 2024
Ayrat (@AtataUruru) I don't see a demonstrated defect here, per #56936
AtataUruru commentedon Jan 8, 2024
Did you look at the examples from #56936 (comment) ?
From the point of view of the algorithm described above, examples T_3 and T_4 should give the same result, but this is not the case
Also in example T_5 it is expected that the result of “infer” will be a specific digit “D”, and not the entire set “Digit”
RyanCavanaugh commentedon Jan 8, 2024
We don't really have the spare resources to look at dozens of examples to figure out which you think are correct and which aren't. Please reduce this to something succinct and demonstrably wrong if you'd like it to be looked at more.
AtataUruru commentedon Jan 8, 2024
first
second