Microsoft/TypeScript

Template literal type inference failure due to lazy placeholder matching

Open

#49,839 opened on Jul 8, 2022

View on GitHub
 (6 comments) (10 reactions) (0 assignees)TypeScript (48,455 stars) (6,726 forks)batch import
BugCursed?Domain: check: Type InferenceHelp Wanted

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.)

Contributor guide