Skip to content

Problem with infer two types from string at same time #56936

Description

@AtataUruru

🔎 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}]`
  : T

It 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

https://www.typescriptlang.org/play?ts=5.4.0-dev.20240102#code/C4TwDgpgBAIglgczsKBeKAGKAfKBGHKAJkIGZCAWQgVkIDZCB2QgDkIE4AoTgeh6jgA7AGYQATlAA2AQwDOKACaJkUAGRQxEeVAD2wqPLFCEAwbsHQAxjsFLgcG51CQoAFS3AA+ngA8rqBAAHsAQtrJQAAYAJADeggCuALYARuIAvhEAfGhuAcGhCuHRMUKiEgBKHmmxpeKweSFhsMrAGZwAkAD8UADalfIANLAAuh0AXFAWAG51fFB+DQVFsQkp6ZkRUGBiOiGWIQpQ8eGpwjqa3HO1EpraegbARoIIQ8AAFqGmZVJyii0GEGstigQLsDkETnA0Hc8k8RAWQUahUiKySqTEGWy6H8iKWKJKIjq-VasXgSFaEQ63Rx+SaxWJ1QJ3xgiyaZOQbXaXV6xKGMFGXIm01m-GEcDE2lByHBWx2ewORxOEDOF3aQogMwkcwRtORxVW6LSG1lu0BCuOUFO5wgl341x+2iU5NeHzM9tuKHuhmMANBIJsYMczmhHk8pB1SOWcTR6SyORpkfx3uejPtLNxbJanOprL1NUJFSqpM53L6Hj5ArVkw1IqgYolKCl9hsJvlEEOFqtqvVmqg2oTeP1MYxxu2pv27cVluV1u4wagIVhCmkwGk3hyAHI8Bg8BvIS5F15l6u4Zvt3v54fPMe1+R0FuL1CF6Gb54qPeMI+Dy+V2vaPev2gK9bniSQvAIdAei3T8hjwUZL1DECwNPSCHyGDB4KfYCtFArw716DcN1gzDv1hJCvHfAiiMwEigMQnDkP-atNTnLDQzwCC3HYnwr1fPBsjmeILECSAJwUHtxH3OjYTwEhsW43jfzhAT+B0ABrCYoN3dDaOfGT8JhcCeJ-E9SBUo5hNEg4JLEKS9PAyjDO8YylyUihzKEoIrPbGzeB4AABYBZAAWi8s1QrEHZbIQmSmKc3xFJPahzPU1jSK8IhOKc+FErXfi+1UjSCPPYi7KvIg5K42EcpMtciBSoqtOojCytDIgDLalyjyUsyCt0RrCNKmKMsczrcrfBrNMGmi-MCkKwv2CKota6q4rG2rPGSvrUpWvCstDcNxvywTLLNHzipg5pyV0q9SEqpzDo2+rtoG7SZuGsMOthR7XNMjzTrEqbqPZYAboO0bvq6683P+hbrKovkWlGPg5tCkTwvEZaPtINbIfGra5h2uY7IAUXR-YIzxR54ggLFOCgBmoAACj8TImYASjQbIByafxugICYiE5jNCnpxnWY5rnchF8JqegfmoEFqBujlxW62kSRZBtUmAEd4g1nwAEEhgAISxKAxZZ1w2c51BudzcJDeV-A1aFh3LYl237ZlqATedgXiE5lWxBptXhA1rW0qsOQtByHoxbJqyfBJvWDavDjXno2RcO8TJMgGBPyeAZPU8kKHZMzsiGIyvOC4ZxOzRL-Wy-T0hK68ciw1rwuk5T5vy4odvPE79z857xu+7T9jqCHzvkrH+ui6bqfqrwWfq9zheoAbinJ5btqiHX7PkPqred+Lveofao+c7Ms+l8v8rB-s4eN9Huvt4f0ur5nl+5+7xevdv63TXn-De-F75AP7rdQ+YDj410gRPYBB025wNvgAz+UCV54WftheBE1EG72Qd9X+eCc7zwLqMIAA

💻 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

Activity

  1. fatcerberus commented on Jan 3, 2024

    @fatcerberus

    The way template string inference works, consecutive placeholders can only have a head/tail relationship. You can’t do it in reverse - infer Rest won’t consume more than one character, as doing so would require backtracking.

  2. AtataUruru commented on Jan 3, 2024

    @AtataUruru
    Author

    The way template string inference works, consecutive placeholders can only have a head/tail relationship. You can’t do it in reverse - infer Rest won’t consume more than one character, as doing so would require backtracking.

    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:

    type Test<T extends string> = T extends `${infer A extends Digit}${infer B extends Digit}${infer C extends Digit}`
    	? [A, B, C]
    	: never
    
    type t_1 = Test<'123'> // [1, 2, 3]
    type t_2 = Test<'1234'> // never
  3. fatcerberus commented on Jan 3, 2024

    @fatcerberus

    This would be closer to the truth

    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:

    1. ${infer Rest} is immediately followed by another placeholder, so always match at least one character.
    2. Rest = 1? Yes, it's unconstrained so that matches; take it and lock it in.
    3. ${infer D extends Digit}
    4. D = 0? Yes, it matches and extends Digit, so lock it in.
    5. End of string? Nope, and we can't backtrack to step 2 or 4 so overall match fails.

    Besides, this works as expected

    Yes, because Digit is 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.

  4. AtataUruru commented on Jan 3, 2024

    @AtataUruru
    Author

    if it work without infer T extends, it have to work with it the same way
    The root of the problem is that these two methods give different results in certain cases:

    T extends `${infer Rest}${Digit}`
    T extends `${infer Rest}${infer D extends Digit}`

    what happens is this

    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

  5. typescript-bot commented on Jan 6, 2024

    @typescript-bot
    Contributor

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

  6. AtataUruru commented on Jan 6, 2024

    @AtataUruru
    Author

    the problem was not resolved

  7. AtataUruru commented on Jan 8, 2024

    @AtataUruru
    Author
  8. RyanCavanaugh commented on Jan 8, 2024

    @RyanCavanaugh
    Member

    Ayrat (@AtataUruru) I don't see a demonstrated defect here, per #56936

  9. AtataUruru commented on Jan 8, 2024

    @AtataUruru
    Author

    Ayrat (@AtataUruru) I don't see a demonstrated defect here, per #56936

    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

    type T_3<T extends string> = T extends `${string}${infer D extends Digit}`
    	? true
    	: false
    
    type T_4<T extends string> = T extends `${infer Rest}${infer D extends Digit}`
    	? true
    	: false

    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”

    type T_5<T extends string> = T extends `${string}${infer D extends Digit}`
    	? D
    	: never
  10. RyanCavanaugh commented on Jan 8, 2024

    @RyanCavanaugh
    Member

    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.

  11. AtataUruru commented on Jan 8, 2024

    @AtataUruru
    Author

    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

    first

    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”

    second

  12. added
    Needs InvestigationThis issue needs a team member to investigate its status.
    and removed
    QuestionAn issue which isn't directly actionable in code
    on Jan 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions