Justin Dalrymple
Justin Dalrymple

Reputation: 886

Typescript function overloading with object - Implementation signatures of overloads are not externally visible

Problem

How can I expose an overloading implementation signature correctly?

Example

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})

Attempted Solutions

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.

Related Links

Typescript Playground link

Upvotes: 8

Views: 4723

Answers (1)

Justin Dalrymple
Justin Dalrymple

Reputation: 886

Solution

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

Thoughts

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

Related Questions