Uh oh!
There was an error while loading. Please reload this page.
Allow pattern literal types like http://${string} to exist and be reasoned about - #40598
Conversation
I had code substantially similar to this in my branch for a while, but decided against keeping it in the initial PR. I really like the core concept if we can overcome the concerns I had. They are:
In general I want to be as minimalistic as possible, but it is not entirely clear where to stop. |
Ryan Cavanaugh (RyanCavanaugh)
commented
Sep 17, 2020
Anders Hejlsberg (@ahejlsberg) I think those are great open questions and ones that we should have some tentative next steps on. The motivating scenario here typeHttpLink<Textendsstring>= `http://${T}>`
functionfn(foo: HttpLink<string>){}fn("wat");just looks so broken without this that I don't feel super comfortable shipping it without at least issuing some kind of interim error when the widening to |
Wesley Wigham (weswigham)
commented
Sep 17, 2020
Anders Hejlsberg (@ahejlsberg)
Having implemented those: Yeah, sure, they do feel similar in that it captures subsets of strings - but regexes are definitely the kitchen sink of string matching, while this is quite focused (and inline with what we've already decided to ship). If anything, considering the reaction within the team, there's some surprise they don't behave like this already - a
Probably! Hopefully the subtype relationship works out for the one half of that, and all that's needed for the other half is simplifying
I wanna say "no" - matching patterns up to patterns, while possible, I don't think is as intuitive as matching literals to patterns - I see leaving it as
I don't think we directly need to handle non-stringy holes quite so much. In fact, non- |
Anders Hejlsberg (ahejlsberg)
commented
Sep 17, 2020
I think an argument can be made that we should handle With respect to matching on |
Wesley Wigham (weswigham)
commented
Sep 17, 2020
While that makes sense (sort-of) for template-to-template relations (so, the current check generalizes to non-string types) where the string parts are similar enough, there's not much of a reasonable (IMO) literal-to-template relation for holes like that (and literal-to-template relations are where these patterns have value validating APIs). Plus, I think such holes are of little practical use, anyway - there's not really a demonstrated need for them, at least not yet (TBH, I think there's a bigger need for things like an |
Chatted about that with Ryan Cavanaugh (@RyanCavanaugh) earlier today, and they're kind of impossible to reason about. What's the implementation for compatibility with |
Trey Brisbane (treybrisbane)
commented
Sep 17, 2020
Sorry to randomly jump in here, but if I may throw a random thought at y'all... 🙂
Given the existence of #40580, would it make sense to introduce a typeNay<Sextendsstring,Nextendsnumber>= `${S}-${N}`;// "Error: Cannot embed non-string type N into string"typeYay<Sextendsstring,Nextendsnumber>= `${S}-${ToString<N>}`;// WorksIMO, this would make the "all inference results are strings" approach much more intuitive, as It would also provide a bit of future flexibility, as it would leave the door open to some hypothetical future Thoughts? |
Anders Hejlsberg (ahejlsberg)
commented
Sep 17, 2020
See #40538. We currently error on types like |
Anders Hejlsberg (ahejlsberg)
commented
Sep 17, 2020
The same as |
Anders Hejlsberg (ahejlsberg)
commented
Sep 17, 2020
That's a possibility, but what would typeFoo<Textendsnumber>= `${ToString<T>}`;constfoo : Foo<number>="bar";// Huh? |
Trey Brisbane (treybrisbane)
commented
Sep 18, 2020
You're correct, that would still be a problem. From a theoretical perspective, So I guess one way to get a "correct" result for |
Wesley Wigham (weswigham)
commented
Sep 18, 2020
Again - we don't have to define these holes such that they have "meaningful" results for all type inputs - we can issue errors if the holes have non-string types in them in checked contexts, and if they get a non-string type via instantiation, do something like what we do for |
I tend to err on the side of picking the conservative Wesley Wigham (@weswigham) did you mean for all primitives? Or everything apart from |
Wesley Wigham (weswigham)
commented
Sep 18, 2020
Everything apart from |
Anders Hejlsberg (ahejlsberg)
commented
Sep 18, 2020
In the original PR there's logic to error when the type of a placeholder is exactly
So I'll say it again, I think the right solution here is to have |
Wesley Wigham (weswigham)
commented
Sep 18, 2020
Anders Hejlsberg (@ahejlsberg) per our discussion in today's design meeting, this now allows |
Wesley Wigham (weswigham)
commented
Sep 18, 2020
So to anyone watching: Reviews would be welcome now~ |
Tom Picton (tpict)
commented
Sep 18, 2020
Is it possible to get a playground of this PR to compare it against #40538? |
Wesley Wigham (weswigham)
commented
Sep 18, 2020
TypeScript Bot (@typescript-bot) pack this |
Hey Wesley Wigham (@weswigham), I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build. |
Tom Picton (tpict)
commented
Sep 19, 2020
Thank you! What is the intended behavior when named pattern literal types are themselves used in a template literal type? For example: typeA= `${number}`;typeB= `${A} ${A}`;// reduces to stringconstexample: B="anything";I'm not sure if it's viable to "unwrap" the types inside a template literal to preserve strictness, but as-is this looks like a variation of #40538. |
Wesley Wigham (weswigham)
commented
Sep 19, 2020
Templates placed into templates should essentially concatenate - good catch; I'm surprised we didn't already have that behavior. |
We did have that behavior early on in the original PR, but I took it out when I added casing modifiers because it gets somewhat more complicated. However, the casing modifiers are going away in #40580, so I'll add the normalization logic back in there. EDIT: Normalization now back in #40580. |
Wesley Wigham (weswigham)
commented
Sep 22, 2020
Anders Hejlsberg (@ahejlsberg) now that #40580 is merged, I've resolved the conflicts, sync'd this, and added an explicit test of concatenating a pattern with another pattern. |
Anders Hejlsberg (ahejlsberg)
left a comment
There was a problem hiding this comment.
Looks good, just a few minor changes.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Tom Picton (tpict)
commented
Sep 23, 2020
Thanks for this, big improvement 🙂 One question/comment about inserting
My question is: in the second case, why don't we preserve the original type declaration instead of taking the product, and evaluate it when attempting assignment? I'm sure there's a technical reason for this, and it should be publicly documented–to take one of Anders Hejlsberg (@ahejlsberg)'s examples, it feels really weird that you can't express a zip code, which on the surface appears to only require up to 50 comparisons to check valid assignments: typeDigit=0|1|2|3|4|5|6|7|8|9;typeZip= `${Digit}${Digit}${Digit}${Digit}${Digit}`;// Errorand yet something like typeExample= `hello ${string}`;has infinitely more permutations, but works fine. |
Anders Hejlsberg (ahejlsberg)
commented
Sep 23, 2020
We favor the normalized string literal union representation because it enables more scenarios. For example, string literal union types can be unioned and intersected with other literal types, can narrow in control flow analysis, can be transformed using distributive conditional types, etc. You're right that in certain cases that lead to very large cross products we might do better by preserving the un-normalized representation and match on that instead. It's an optimization we could consider, though I don't know how practical it is or how useful it would really be. |
Tom Picton (tpict)
commented
Sep 23, 2020
That makes total sense! I would appreciate the optimisation being considered–I imagine there's a negative correlation between cases where "the normalised representation of this type is oversized" and cases where "I want to put this type in a switch statement or map over it". Template literal types are a big win for TS users even when they're used for string validation alone. It's a little sad to not have that feature in scenarios that would be incompatible with features (mapping, control flow etc) that wouldn't make a whole lot of sense to use with a union of that complexity anyway. I guess there's a question of "how does one communicate the difference between normalised/unnormalised template types to TypeScript users?", and the solution might lie somewhere in the delineation of applications (validation vs transforming & control flow). |
So it looks like this person wants a type like |
Joe Calzaretta (jcalz)
commented
Jan 17, 2021
heh, this question wonders why pattern literals can't usually be matched via typeSplitComma<T>=Textends `${infer L},${infer R}` ? [L,R] : never;typeOkay=SplitComma<`left,right`>// ["left", "right"]typeNotOkay=SplitComma<`left,${string}`>// never!or even typeSimpler<T>=Textends `${infer U}` ? U : never;typeOkay1=Simpler<"abc">// abctypeOkay2=Simpler<`${string}`>// stringtypeNotOkay1=Simpler<string>// never!typeNotOkay2=Simpler<`a${string}`>// never! |
There's no open issue, but we mentioned how
/${string}simplified to juststringwas a bit of a shortcoming in teams, and it looked easy enough to change, so here it is. With this change, we can now reason over nongeneric patterns and match them, allowing things like: