-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Support C# / Rust-style "where" syntax for generic parameters #42388
Comments
Scoped type aliases are enough to get my vote, but does it have to follow Rust's syntax? Might there be a better syntax to do this? |
@00ff0000red it doesn't have to follow Rust's syntax exactly, I mostly offer that as evidence that it's not a completely crazy idea (and I assume much of the discussion in the Rust RFC would also be relevant to TypeScript). |
Seems good. But I guess there must once be a good reason for not using "where" as generic constraint. Because C# does use similar syntax https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters They are designed by the same person, so... I am curious too :) |
Good point @ShuiRuTian, now I feel sheepish about calling this Rust-style syntax :) Updated the title of the issue and added a note. |
Looking at the Rust RFC, another motivator there was being able to specify constraints that involved multiple type parameters. So something like: const users = [
['John', 'Doe'],
['Bobby', 'Droppers'],
] as const;
function greet<First, Last>(first: First, last: Last)
where [First, Last] extends typeof users[number] {
// ...
} I wouldn't say this is common, but I can think of a few situations I've run into where it would have been convenient. |
So it's actually not a syntax sugar, it's introduced a new way to restrain the generic types right? |
If the constraint involves multiple parameters, yes. That wouldn't be in v1 of this proposal, I just wanted to flag it as a possibility that this syntax opens up. |
I don't love the syntactic positioning of this or the idea that it would be a place to introduce type parameters, but I'm compelled by this as an alternative approach to some other feature requests (specifically #14520 upper-bounded generics). You could imagine writing things like this interface Array<T> {
// Proposing that "where" go at the end of the type parameter list
includes<U where T extends U>(el: U): boolean
} |
Also, if the declare function assertEqual<T, U where U extends T, T extends U>(actual: T, expected: U): boolean;
const g = { x: 3, y: 2 };
// Would correctly error
assertEqual(g, { x: 3 }); |
Or perhaps declare function assertEqual<T, U where [U, T] extends [T, U]>(actual: T, expected: U): boolean; |
The topic of scoped types aliases came up again recently as a tangent in #26349 (on partial inference). This issue seems maybe the most lively/promising place to move the discussion to where it's still on-topic. To briefly summarize the tangent, it started when I mentioned a gist where I explore the synergy between partial inference and what I've called "private types" (e.g. Stepping back, let me try to summarize the bigger picture. There are three main contexts where type aliases are useful:
These contexts are unfortunately somewhat opposed to each other: e.g. #23188 and #41470 both propose "type node" syntaxes, which support inline use in mapped types, but wouldn't (directly) benefit the classes/functions case at all (whereas e.g. #7061 wouldn't help the inline case). Syntax proposals that "live inside the In addition to simply local aliases, I also want to call attention to several distinct-but-related user needs, which have varying degrees of relevance across the three contexts:
The proposed syntax options include the following:
The original tangent was about "private types". As referenced above, putting these into the bracketed type parameter list is potentially a mistake, but it's currently the only hammer we have available, so I think a lot of developers are biased in favor of it. Even without direct language support for private type parameters, it's possible to at least partially emulate it with an impossible-to-write optional type parameter, though this only works when inference is off (i.e. the non-optional parameters are explicitly specified). I'd therefore consider definition-site partial inference and arbitrary constraints to be the two most important requirements (with aliasing being a nice bonus, but not strictly required). The former should hopefully follow from #26349, so the main question to me is how to express arbitrary constraints. I like function foo<T satisfies SomeCondition<U> extends true ? T : never, U extends T>(...) and it's likely to be relatively straightforward to implement in place. |
Suggestion
🔍 Search Terms
✅ Viability Checklist
⭐ Suggestion
Instead of writing:
You'd also be allowed to write:
It might even be preferable to leave
T
andK
off the generic parameters list (the bit inside<..>
), since the intent is most likely for them to be inferred based onobj
andkey
, but we'll get to this later.For a type alias, instead of:
you'd also be able to write:
This mirrors an identical syntax in Rust (see also Rust RFC 135) (update: and also C#, so I guess Anders knows about this!). It would solve three distinct problems:
Legibility
When a function or type alias has many generic arguments, each with an
extends
clause and a default value, it can get difficult to pick out what the type parameters are, or even how many of them there are. Awhere
clause lets you push the generic bounds and defaults outside the parameter list, improving its legibility.Scoped type aliases
There's no easy type-level equivalent of factoring out a variable to eliminate duplicated expressions like you would in JavaScript. A
where
clause would make it possible to introduce type aliases that don't appear in the generic parameter list.Partial inference for functions
See Allow skipping some generics when calling a function with multiple generics #10571. It's not currently possible to specify one type parameter for a generic function explicitly but allow a second one to be inferred. By creating a place to put types that's not the parameter list, a
where
clause would make this possible.There are examples of all three of these in the next section.
💻 Use Cases
Legibility
There are many examples of complicated generic parameter lists out there. Here's one chosen at random from react-router:
It's clearer that there are two type parameters if you move the constraints and default values out of the way:
The existing workaround for this is to put each type parameter on its own line:
It's a judgment call which you prefer. I find the other two uses more compelling!
Scoped type aliases
With complicated generic types and functions, it's common to have repeated type expressions. Here's a particularly egregious example (source):
wow that's a lot of repetition! Here's what it might look like with
where
:By introducing some local type aliases in the
where
list, we're able to dramatically reduce repetition and improve clarity. We should actually removePath
from the type parameters list since the intention is for it to be inferred, but let's save that for the next example.Existing workarounds include factoring out more helper types to reduce duplication, or introducing an extra generic parameter with a default value, e.g.:
This still repeats the type expression twice (
SafeKey<API[Path], 'get'>
), but since it's used three times, it's a win! This is kinda gross and creates confusion for users about whetherSpec
is a meaningful generic parameter that they'd ever want to set (it isn't).Partial inference for functions
Sometimes you want to infer one generic parameter to a function and have the others be derived from that (#10571). For example (following this post):
This fails if you pass
API
explicitly and try to letPath
be inferred:This problem could be solved by using
where
to introduce a type parameter that's not part of the generics list:This would allow
Path
to be inferred from thepath
parameter while still specifyingAPI
explicitly and enforcing theextends keyof API
constraint. The only workarounds I'm aware of now involve introducing a class or currying the function to create a new binding site:A
where
clause would help with the general problem that there are two reasons to put a type parameter in the generic parameters list for a function:and there's no syntax for distinguishing those. A
where
clause would let you do that.The text was updated successfully, but these errors were encountered: