-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
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. |
@ahejlsberg I think those are great open questions and ones that we should have some tentative next steps on. The motivating scenario here type HttpLink<T extends string> = `http://${T}>`
function fn(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 |
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- |
I think an argument can be made that we should handle With respect to matching on |
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 @RyanCavanaugh earlier today, and they're kind of impossible to reason about. What's the implementation for compatibility with |
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 type Nay<S extends string, N extends number> = `${S}-${N}`; // "Error: Cannot embed non-string type N into string"
type Yay<S extends string, N extends number> = `${S}-${ToString<N>}`; // Works IMO, 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? |
See #40538. We currently error on types like |
The same as |
That's a possibility, but what would type Foo<T extends number> = `${ToString<T>}`;
const foo : Foo<number> = "bar"; // Huh? |
You're correct, that would still be a problem. From a theoretical perspective, So I guess one way to get a "correct" result for |
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 @weswigham did you mean for all primitives? Or everything apart from |
Everything apart from |
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 |
@ahejlsberg per our discussion in today's design meeting, this now allows |
So to anyone watching: Reviews would be welcome now~ |
Is it possible to get a playground of this PR to compare it against #40538? |
@typescript-bot pack this |
Heya @weswigham, I've started to run the tarball bundle task on this PR at 916aec6. You can monitor the build here. |
Hey @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. |
Thank you! What is the intended behavior when named pattern literal types are themselves used in a template literal type? For example: type A = `${number}`;
type B = `${A} ${A}`; // reduces to string
const example: 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. |
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. |
@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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just a few minor changes.
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 @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: type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`; // Error and yet something like type Example = `hello ${string}`; has infinitely more permutations, but works fine. |
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. |
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 |
heh, this question wonders why pattern literals can't usually be matched via type SplitComma<T> =
T extends `${infer L},${infer R}` ? [L, R] : never;
type Okay = SplitComma<`left,right`> // ["left", "right"]
type NotOkay = SplitComma<`left,${string}`> // never! or even type Simpler<T> = T extends `${infer U}` ? U : never;
type Okay1 = Simpler<"abc"> // abc
type Okay2 = Simpler<`${string}`> // string
type NotOkay1 = Simpler<string> // never!
type NotOkay2 = Simpler<`a${string}`> // never! |
There's no open issue, but we mentioned how
/${string}
simplified to juststring
was 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: