Skip to content

Named/keyed type parametersΒ #54254

Open
Open
@Andarist

Description

@Andarist

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:

  1. we can't easily default them
  2. 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)
  3. 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
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions