Description
Suggestion
π Search Terms
- mapped type
- type mapping
- generics
- generic function
- unknown
β 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
Performing type mapping on a generic type should result in a new generic type with that mapping applied, rather than a concrete type with unknown
in place of generics.
π Motivating Example
Consider the following utility type Async<T>
. It takes an arbitrary function signature and returns the signature of an equivalent asynchronous function.
type Async<T extends (...args: any) => any> = T extends (...args: infer Args) => infer Result
? (...args: Args) => Promise<Result>
: never;
type SimpleFunction = (arg: number) => string;
type AsyncSimpleFunction = Async<SimpleFunction>;
// result: (arg: number) => Promise<string> β
Now let's give it a generic function:
type GenericFunction1 = <T>(arg: T) => string;
type AsyncGenericFunction1 = Async<GenericFunction1>;
// expected: <T>(arg: T) => Promise<string>
// actual: (arg: unknown) => Promise<string> βοΈ
type GenericFunction2 = <T>(arg: number) => T;
type AsyncGenericFunction2 = Async<GenericFunction2>;
// expected: <T>(arg: number) => Promise<T>
// actual: (arg: number) => Promise<unknown> βοΈ
As this example shows, we cannot use the helper type Async<T>
with a generic function signature. If we try, we get a non-generic function signature with unknown
wherever a generic argument was used.
π» Use Cases
We maintain a plugin system. Plugins are developed against a carefully designed API. For every type in the plugin API, there is a corresponding internal type. Plugin values and internal values are identical at runtime, but their types differ. For instance, the same concept may be represented by a class internally, but by a branded interface towards the plugins.
To convert between an internal type and its corresponding plugin type, we use a recursive mapped type. This words great, unless the value is (or contains) a generic function.