Reputation: 886
How can I expose an overloading implementation signature correctly?
Building off of this question:
interface MyMap<T> {
[id: string]: T;
}
type Options = {
asObject?: boolean,
other?: Function
testing?: number
};
function get(options: { asObject: true, other?: Function, testing?:number }): MyMap<any>;
function get(options: { asObject?: false, other?: Function, testing?:number }): number[];
function get(): any[];
function get(options: Options = { asObject: false }): any[] | MyMap<any> {
if (options?.asObject) return {} as MyMap<any>;
return [];
}
How can I wrap this function but retain the possible return types depending on the options argument?
For example:
function wrapFunction(arg1 arg2, options) {
// do something with arg1 and arg2
return get(options)
}
Depending on the value set for options in the wrapFunction method, the return type would be based on the return type of get with that same value.
ie:
const g = wrapFunction(1,2, {asObject: true})
// Should return the same thing as get({asObject:true})
I could simply rewrite a new signature for the wrapFunction, but this would be quite verbose, especially if I have many types of wrapFunctions that follow the same pattern of having a nested get
call.
One suggestion was to type cast the wrapFunction as typeof get
but this removes the ability to modify the parameters list of wrapFunction.
This might be related.
Upvotes: 8
Views: 4723
Reputation: 886
To fix this problem I made use of conditional types. For example:
// This is the switch interface
interface Switch<T> {
on: T
}
// This is the conditional type.
type ExtendReturn<E extends boolean, R extends string | number > = E extends true
? Array<R>
: number;
/*
First solution, using the conditional type and type inference. This requires a
nested structure and returns a curry-like function. The inner function separates out all the generics that should be inferred, and the outer function contains the generic that should be set explicitly
*/
function test3<R extends string | number = string>() {
return function inner<E extends boolean>(options: Switch<E>): ExtendReturn<E, R> {
const r = options.on ? 's' : 4
return r as ExtendReturn<E, R>
}
}
// Notice the extra (), but each of the types are inferred correctly
const a = test3<number>()({ on: true })
/*
Second Solution, using a combination of overload methods, and the conditional
type. This is a bit more verbose, but removes the requirement of the currying
structure, keeping a cleaner API.
*/
function test4<R extends string | number = string>(options?: Switch<false>): ExtendReturn<false, R>;
function test4<R extends string | number = string>(options: Switch<true>): ExtendReturn<true, R>;
function test4<E extends boolean, R extends string | number = string>(options?: Switch<E>): ExtendReturn<E, R> {
const r = options?.on ? 's' : 4
return r as ExtendReturn<E, R>
}
// Notice the simpler API
const m = test4<string>({ on: true })
The full comparison can be seen here
These are workarounds for this issue, but sufficiently solve the problem. For complex scenarios, or perhaps a hidden API, using the currying method looks cleaner, avoiding the need for many overload methods. Similarly, more basic scenarios or public API's lend themselves to the second solution.
Upvotes: 1