้•œๅƒ็ซ™็‚น ยท ๆœฌ้กต็”ฑ็ฌฌไธ‰ๆ–น GitHub ๅช่ฏป้•œๅƒๆไพ›๏ผŒ้ž GitHub ๅฎ˜ๆ–น็ซ™็‚น๏ผŒไธๆŽฅๅ—ไปปไฝ•็™ปๅฝ•ๆˆ–ๅ‡ญๆฎ่พ“ๅ…ฅใ€‚ๅ‰ๅพ€ github.com
Skip to content

Concatention of string index templates ${number}${string}${number} is not additive w.r.t. accepted strings.ย #46124

Description

Bug Report

๐Ÿ”Ž Search Terms

String index templates
Template String Pattern Index Signatures

๐Ÿ•— Version & Regression Information

Seen on 4.4.3

  • String Index Templates are a new feature with 4.4.2/3

โฏ Playground Link

playground

๐Ÿ’ป Code

// Violates additivity
type T1 = Record<`${string}`,any>
const t1:T1 = {'':0} // pass (expected)
type T2 = Record<`${number}`,any>
const t2_1spaces:T2 = {' ':0} // pass (expected)
type T3 = Record<`${number}${string}`,any>
const t3_1spaces:T3 = {' ':0} // pass (expected) 
type T4 = Record<`${number}${string}${number}`,any>
const t4_0spaces:T4 = {'':0} // fail (expected) 
const t4_1spaces:T4 = {' ':0} // fail (expected)
const t4_2spaces:T4 = {'  ':0} // fail (UNEXPECTED)   <------------
const t4_3spaces:T4 = {'   ':0} // pass (expected) 
const t4_4:T4 = {'11':0} // fail (UNEXPECTED)         <------------
const t4_5:T4 = {'111':0} // pass (expected)

type X<T extends number|string> = Record<T,any>

type NS = `${number}${string}` 
type N = `${number}` 
type NSN = `${NS}${N}`

type A=X<NS>
type B=X<N>
type C=X<NSN>
const a:A = {'1':null} // pass (expected)
const b:B = {'1':null} // pass (expected)
const c:C = {'11':null} // fail (UNEXPECTED)  <---------------------------

๐Ÿ™ Actual behavior

If two templates are concatenated, that result will not accept all concatenations of originally accepted strings.

(More narrow assessment: ${number}${string}${number} should be equivalent to ${number}${string}, but isn't.)

๐Ÿ™‚ Expected behavior

If two templates are concatenated, that result will accept all concatenations of originally accepted strings.

(More narrow assessment: ${number}${string}${number} should be equivalent to ${number}${string}.)

Activity

  1. ahejlsberg commented on Sep 29, 2021

    @ahejlsberg
    Member

    This is working as intended, but I agree it may be a bit surprising. Quoting from the original PR for this feature, here's how matching works:

    Type inference supports inferring from a string literal type to a template literal type. For inference to succeed the starting and ending literal character spans (if any) of the target must exactly match the starting and ending spans of the source. 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.

    Note the last sentence. When placeholders are immediately next to each other, the first placeholder just infers a single character from the source. The kind of placeholder being inferred to doesn't matter for determining the inferred text, it only matters for validating the text. The rationale for the single character behavior is to provide a mechanism for picking apart string literals using inference in conditional types, as in S extends `${infer Char}${infer Rest}` ? .... It is less useful with non-generic placeholders, but the behavior is the same for consistency and because we use a single matching algorithm. That algorithm is fairly simple, but that is by design. We really don't want to turn our type system into another RegEx engine.

  2. craigphicks commented on Sep 29, 2021

    @craigphicks
    Author

    Thanks for the detailed answer and the pointer to the very clear documentation. Now I understand why a final ${string} can accept the empty string and can code accordingly.

  3. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions