Skip to content

Type 'x.y.${Name}' does not satisfy the constraint '${string}.${Name}'.(2344) #61990

Open

Description

🔎 Search Terms

Template Literal Types, assignability, generics

🕗 Version & Regression Information

  • Between versions 4.3.5 and 4.2.3 more cases like this started being supported ( x.${Name} became assignable to ${string}.${Name} for example), however the x.y.${Name} case has not bees assignable in any version since template support was added up to and including current nightly ( 5.9.0-dev.20250702 )

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.0-dev.20250702#code/PQKhCgAIUhhB7AtgBwJYBsCmkAurHYCGAzsZgE464AWhVAgpKsZCcagOYB2hARlrniQAFJgAeOTFwAmxAJSQAQgDooMACpCArmQA0kdinQBPSNMwAzVFyK5jyTAC41kAAY572APoBjaph8Aa0gAXkhyTABHLVQI+lJOHn5MTQAedX0AUTEHH0lpAD4Ablc1YHBxZHhKOwdwqJi4hO4+LDSvRnFJGRZFfUUC0NxyLUwi8HAPOoB3asDiAGVrHxTPBZ9yVGQcABYvAGZUgDlCAkguqVkDHE2uDkGwiOjYzHj2FuS0qEgf1zFlAAkAG8TgQAL6uXTfX7A4g3awcMGAkGnTAQ8DFCZTbAWQgYYjHVHnCSXFhw273IZPRqvZpJNrwVLQtz-YzI0FoyHM1yw+F3JHAjnozGTTyQLjwLgAcSkFFQPgA6nMWI8Gi83olWilGdzWcoAF5cn4woHkhFIw0YopAA

💻 Code

/**
 * Compile time assert that A is assignable to (extends) B.
 * To use, simply define a type:
 * `type _check = requireAssignableTo<T, Expected>;`
 */
export type requireAssignableTo<_A extends B, B> = true;

type worksSinceTypeScript4_3<Name extends string> = requireAssignableTo<
    `x.${Name}`,
    `${string}.${Name}`
>;

type fails<Name extends string> = requireAssignableTo<
    // Type '`x.y.${Name}`' does not satisfy the constraint '`${string}.${Name}`'.(2344)
    `x.y.${Name}`,
    `${string}.${Name}`
>;

type nonGenericWorks = requireAssignableTo<
    `x.y.z`,
    `${string}.z`
>;

🙁 Actual behavior

A compile error:

Type 'x.y.${Name}' does not satisfy the constraint '${string}.${Name}'.(2344)

🙂 Expected behavior

No compile error. The .${Name} portion of the type matches exactly, so this can be reduced to checking if "x.y" is assignable to string should be allowed.

Additional information about the issue

I understand that in general not all cases like that are practical to support: perfect assignability checking of template strings is likely infeasible but I decided to report this case as I think this one suggest an algorithm that would be practical to implement and efficient: running a greedy algorithm to remove common constant substrings from the beginning and end of the string, including generic values, might be a viable approach, and would improve handling of this case.

Activity

  1. MartinJohns commented on Jul 2, 2025

    @MartinJohns
    Contributor

    I believe this is working as intended, as per #40336.

    Inference proceeds by matching each placeholder to a substring in the source from left to right: A placeholder followed by a literal character span is matched by inferring zero or more characters from the source until the first occurrence of that literal character span in the source. A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.

    Basically the matching is not eager. The ${string} part in ${string}.${Name} will only match up to the first ., and ${Name} will match all the rest. So it's different from x.y.${Name}, because here the part after the first . will not be matched by ${Name}.

  2. CraigMacomber commented on Jul 2, 2025

    @CraigMacomber
    Author

    I believe this is working as intended, as per #40336.

    Inference proceeds by matching each placeholder to a substring in the source from left to right: A placeholder followed by a literal character span is matched by inferring zero or more characters from the source until the first occurrence of that literal character span in the source. A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.

    Basically the matching is not eager. The ${string} part in ${string}.${Name} will only match up to the first ., and ${Name} will match all the rest. So it's different from x.y.${Name}, because here the part after the first . will not be matched by ${Name}.

    By that augment, the fact that x.y.z is assignable to ${string}.z seems like a bug then since the string is just supposed to match up to the first "." and .y.z is not equal to .z?

    Perhaps I misunderstand, but that sounds like it would be a really odd choice, and also isn't how simpler cases behave.

  3. MartinJohns commented on Jul 2, 2025

    @MartinJohns
    Contributor

    By that augment, the fact that x.y.z is assignable to ${string}.z seems like a bug then since the string is just supposed to match up to the first "." and .y.z is not equal to .z?

    In that case it knows that the string will end with .z, but in the case with ${Name} it doesn't know it, and ${Name} is matching all the rest (after the first .).

  4. CraigMacomber commented on Jul 2, 2025

    @CraigMacomber
    Author

    In that case it knows that the string will end with .z, but in the case with ${Name} it doesn't know it, and ${Name} is matching all the rest (after the first .).

    You have explained how it works, and why the implementation produces the observed results. Thanks for that.

    Perhaps I'm misinterpreting your meaning but from that being the entirety of your response I'm interpreting that as implying that this is not a bug, and is expected behavior. Is that correct?

    If this is expected, that's fine, but in that case I think the documentation should better explain this kind of thing, or at least note that there are some limitation.

    Either way I still think the behavior I would like here would be safe, desirable, compatible with what is documented about template string literals, and possible to implement. I'm not clear if the above comment is intended to disagree with that implicitly or simply doesn't address it.

    To me it seems like the choice to implement it as currently implemented produces very strange behaviors which do not make sense and require understanding the implementation details of the compiler to discover these behaviors.

    For example, it's not intuitive to me that if you were to replace the name type parameter with any possible type (string, any, never any union of string literals, or even a nested template literal) it is assignable, but it's unassignable when the generic type parameter is used. I understand if thats an implementation limitation, but it seems undesirable and undocumented.

    It's also unclear to me from my reading of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#handbook-content why we get different behavior if we reverse the string. Nothing in the documentation implies that the template string literals check assignability in left to right in a greedy way, which seems to be what's implemented.

    Consider this case:

    type requireAssignableTo<_A extends B, B> = true;
    
    type forwards<S extends "A"> = requireAssignableTo<`${S}.y.x`,`${S}.${string}`>; // Compiles
    type reversed<S extends "A"> = requireAssignableTo<`x.y.${S}`,`${string}.${S}`>; // Errors

    Is there a way a reader of the documentation should come to the conclusion that the direction of strings should impact which cases are allowed?

    To me this behavior is surprising, especially since some other things in TypeScript do not work like this. For example doing the same thing with arrays instead of separated strings allows both cases to compile:

    type forwardsArray<S extends unknown[]> = requireAssignableTo<[...S,"y","x"],[...S,...string[]]>; // Compiles
    type reversedArray<S extends unknown[]> = requireAssignableTo<["x","y",...S],[...string[],...S,]>; // Compiles

    It seems odd to me that arrays handle this case fine, but template literal strings behave differently.

    I know TypeScript accepts some limitations to which cases compile and which cases do not, and some edge cases (like those around recursive types) are fuzzy and not guaranteed. If there is a similar area of unspecified best effort support for template type literals, I personally think it would be nice to document that somewhere. I checked the handbook page and can't find any indication of such limitations, but maybe there is another place I should look?

    Anyway, if this really is expected, feel free to convert this into a feature request (for more cases to be supported and/or improved documentation) instead of a bug report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions