Skip to content

Introduce mechanism for manipulating function types without losing generic type informationΒ #50481

@TheUnlocked

Description

@TheUnlocked

Suggestion

πŸ” Search Terms

  • generic function add parameter
  • parameters of generic function
  • prepend parameter to generic function

βœ… 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

Being able to manipulate generic function types without losing the generic type parameters would be very useful (see under motivating example). Coming up with a good syntax for it is not easy, but I see a few possible approaches (precise syntax/names subject to change):

  1. A bunch of utility types (inelegant but gets the job done)
type F = <T, R>(t: T) => R;

type X1 = WithReturnType<F, number>; // X1 = <T, R>(t: T) => number;
type X2 = WithParameters<F, [x: number]>; // X1 = <T, R>(x: number) => R;
type X3 = PrependParameter<F, number>; // X1 = <T, R>(arg0: number, t: T) => R;
type X4 = AppendParameter<F, number>; // X1 = <T, R>(t: T, arg0: number) => R;
type X5 = ReplaceParameter<F, 0, number>; // X1 = <T, R>(t: number) => R;
  1. Enhancement of extends as is (probably the prettiest solution but may present challenges when speccing it out):
type F = <T, R>(t: T) => R;

type X1 = F extends (...args: infer Args) => infer R
    ? (x: number, ...args: Args) => R
    : never;
// ...
  1. Enhancement of extends with new syntax to declare intent (this is much more general but would only make sense when combined with a broader "generic generics" or HKT feature):
type F = <T, R>(t: T) => R;

type X1 = F extends <...infer TypeArgs>(...args: infer Args) => infer R
    ? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
    : never;
// ...

// or maybe
type X1 = F extends <...infer TypeArgs>(...args: infer Args<...TypeArgs>) => infer R<...TypeArgs>
    ? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
    : never;
// ...
  1. Alternative extension to existing syntax (in general, but here's one possibility):
type F = <T, R>(t: T) => R;

type X1 = <...Args>(x: number, ...args: Parameters<F<...Args>>) => ReturnType<F<...Args>>;

πŸ“ƒ Motivating Example

Consider some kind of API/middleware builder that looks like this when used:

builder
    .add('slow', { rate: 500 })
    .add('get', { query: ['abc'] as const }, ({ query: { abc } }) => {
        // ...
    };

It could be typed as something like this:

interface Builder {
    add(type: 'slow', options: { rate: number }): Builder;
    add<Q extends readonly string[]>(
        type: 'get',
        options: { query: Q },
        callback: (data: { query: { [K in Q[number]]: string } }) => void
    ): Builder;
    // ...
}

However, now we want third parties to be able to add their own middleware types to the builder. Making an API for adding them is fairly easy:

BuilderFactory.register('merge', (options, callback) => {
    // ...
    callback(...);
    // ...
} );

But typing it is much harder. Usually we would use module augmentation to allow third parties to type their middleware types:

interface Builder {
    add<T extends keyof BuilderMethods>(type: T, ...args: Parameters<BuilderMethods[T]>): ReturnType<BuilderMethods[T]>
}

// third party code
declare module '...' {
    interface BuilderMethods {
        merge: <Q extends readonly string[]>(
            options: { query: Q },
            callback: (data: { query: { [K in Q[number]]: string } }) => void
        ) => void;
    }
}

But if we do this, we lose all of the information encoded in the type parameters. This feature would allow the add method to retain the generic type information of the function type it's derived from.

πŸ’» Use Cases

My use case would be relatively similar to the motivating example above. Currently my workaround is to just only have a good typing experience for first-party methods, but that's not ideal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions