Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
craigphicks opened this issue Sep 28, 2021 · 2 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@craigphicks
Copy link

craigphicks commented Sep 28, 2021

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

@ahejlsberg
Copy link
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.

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 29, 2021
@craigphicks
Copy link
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants