How to have functions pass arguments with the same overloads?

Viewed 4055

I am trying to create a function passes its arguments to another function. Both of these functions need to have the same overloads.

function original (a: number): boolean;
function original (a: string, b: string): boolean;
function original (a: number | string, b?: string): boolean {
  return true;
}

function pass (a: number): boolean;
function pass (a: string, b: string): boolean;
function pass (a: number | string, b?: string): boolean {
  return original(a, b);
}

This does not work.

Argument of type 'string | number' is not assignable to parameter of type 'string'.

Type 'number' is not assignable to type 'string'.(2345) input.tsx(4, 10): The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

Playground

4 Answers

You can just declare your pass function as being of the type of the original function using the typeof operator, e.g.

function original (a: number): boolean;
function original (a: string, b: string): boolean;
function original (): boolean {
  return true;
}

let pass: typeof original =  () => {
  return true;
};

pass(5);         // all good
pass('a', 'b');  // all good
pass(45, 'b');   // error here

Playground here

Add extra overload:

function original<
  A extends number | string,
  B extends A extends string ? string : undefined
>(a: A, b: B): boolean;

Now it looks like:

function original(a: number): boolean;
function original(a: string, b: string): boolean;

function original<
  A extends number | string,
  B extends A extends string ? string : undefined
>(a: A, b: B): boolean;

function original(a: number | string, b?: string): boolean {
  return true;
}

function pass(a: number): boolean;
function pass(a: string, b: string): boolean;
function pass(a: number | string, b?: string): boolean {
  return original(a, b); // Works
}

const t1 = original(1); // Works
const t2 = original("foo", "foo"); // Works
const t3 = original(1, "foo"); // Works: gives an error as expected
const t4 = original("foo"); // Works: gives an error as expected

You no more need the second overload function original(a: string, b: string): boolean but you may still leave it for better readability of the code.

Playgroun

Here's a solution which doesn't use type assertions and doesn't change or add any overload signatures to either function: use an IIFE to create both functions, where the functions have the weaker signature inside the IIFE's "private" scope, but the stronger signature outside.

interface OriginalSignature {
  (a: number): boolean;
  (a: string, b: string): boolean;
}

const [original, pass] = (function(): [OriginalSignature, OriginalSignature] {
  function original(a: number | string, b?: string): boolean {
    return true;
  }
  function pass(a: number | string, b?: string): boolean {
    return original(a, b);
  }
  return [original, pass];
})();

Tests:

// OK
original(1);
original('a', 'b');
pass(1);
pass('a', 'b');

// errors
original('a');
original(1, 'b');
pass('a');
pass(1, 'b');

Playground Link

Cast to any in implementation ;). With overloaded signatures you are sure nothing else than allowed arguments will be there.

function original (a: number): boolean;
function original (a: string, b: string): boolean;
function original (a: number | string, b?: string): boolean {
  return true;
}

function pass (a: number): boolean;
function pass (a: string, b: string): boolean;
function pass (a: number | string, b?: string): boolean {
    return original(a as any, b as any); // here, you are sure only 
    // expected arguments may appear - so the cast operation is safe.
}

Alternatively use named tuples instead of overloading (the effect is similar to overloading).

function original(...args: [a: string, b: string] | [a: number]): boolean {
  return true;
}

function pass (...args: [a: string, b: string] | [a: number]): boolean {
  return original(...args);
}
Related