Description
Suggestion
π Search Terms
named type parameters generic bag
β Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
Disclaimer: I somewhat suspect that a similar feature request exists already but I couldn't find any.
I'd like to suggest an ability to specify named type parameters. Some libraries have long lists of generic type parameters (XState currently sits at 5: here) and it becomes hard for consumers to remember the order of those.
I think that it might be hard for me to beat @weswigham's arguments from here:
This could be particularly useful in Vue, as then they could introduce a bag for the type parameters passed into their many generic functions. This would enable them to add more type arguments as needed for improved inference or for future features, without breaking existing consumers who are manually specifying a subset of parameters (simply make new arguments in the bag optional) and also allowing that bag to be extended (via interface reopening) by any vue plugins, such as vuex (which then enables them to add overloads to vue-core methods which can carry through the new type information they provide).
I think that it's best to model this feature after objects and destructuring patterns. That should create a familiar syntax for end users.
I'm not really married to any particular syntax and I think it's somewhere up for debate, one variation that comes to mind is this:
declare function fn<{ T extends number; T2 extends string; }>(a: T, b: T2): void
fn<{ T: 100, T2: 'foo' }>(100, 'foo')
π Motivating Example
Any library with more than 2-3 type parameters. Vue, TanStack Query, XState and more come to mind.
Currently TanStack query has such an overload for its useQuery
:
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'initialData'
> & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>
This could likely be rewritten to something like:
export function useQuery<{
fnData = unknown,
error = unknown,
data = fnData,
key extends QueryKey = QueryKey,
}>(
options: Omit<
UseQueryOptions<fnData, error, data, key>,
'initialData'
> & { initialData: fnData | (() => fnData) },
): DefinedUseQueryResult<data, error>
A nice trait of this feature would be that generic names would become autocompletable. Today we might rely on signature help when typing but it doesn't provide the ideal experience. It has too much information (even if the interesting piece of information is in bold), it's squished into a single line and it's hard to focus on what we are typing (plus, of course, we can't selectively start typing those type parameters "out of order").
π» Use Cases
Currently one might use a "generic bag" to imitate this feature. Using the previous example this could look like:
export function useQuery<T extends { fnData: unknown; error: unknown; data: unknown; key: QueryKey }>(
options: Omit<
UseQueryOptions<T['fnData'], T['error'], T['data'], T['key']>,
'initialData'
> & { initialData: T['fnData'] | (() => T['fnData']) },
): DefinedUseQueryResult<T['data'], T['error']>
One of the problems with this is that it's not easy to add defaults to specific slots, one has to resort to helper types like WithDefaults
and implement that logic on their own.
In fact, I currently have an open PR that would allow to infer T
using such indexes like in the example above (this PR builds on top of an already referenced @weswigham's PR).
My primary motivation is to enable such inference (based on indexes) for reverse mapped types. It's important for those to create multiple relationships within a single object property value/tuple element. It could be a nice addition for other type parameters that would make this feature more consistent. However, I think that when it comes to regular type parameters this approach has an important disadvantage when compared to this proposal.
Inferring using indexes doesn't provide the same capabilities as inferring to naked type parameters:
- we can't easily default them
- we can't rely on different sets of type parameter modifiers (
in
/out
/const
) for each "slot" (it's not possible to annotate part of the type parameter) - it's hard to reason about variance of those "slots", I'm not even quite sure how such indexes behave when it comes to variance
- the upcoming partial inference sigil won't be applicable for such a "slot"
I think that this featue is important to address those concerns and to enhance the flexibility for library authors.
When it comes to the implementation... I think that this could be added fairly easily. The main things that would have to be added to add support for this would be the changes in the parser and code "matching" the names with the parameters list. The parameters lists should still be kept internally as a flat array and matching would be implemented only on boundaries.