5

Basically I'm just trying to wrap a function that has multiple signatures. Is there any clean way to do this without re-hardcoding all of the signatures? I just want bar below to accept any of the same arguments as foo

declare function foo(x: string): number;
declare function foo(x: string, y: number): 42;
declare function foo(x: number): string;

type fooArgs = Parameters<typeof foo>; // this only extracts from the last signature
declare function bar(...args: fooArgs): string[]

const a = foo(123);        // fine
const b = foo('123');      // fine
const c = foo('123', 456); // fine

const x = bar(123);        // fine
const y = bar('123');      // fails because it expects (x: number) => string
const z = bar('123', 456); // fails

ts playground

2 Answers2

1

This is by design. It always returns last overloaded signature. See this issue/28789

However, there is another way.

Please keep in mind that overloading is just an intersection of function types.

We can declare a type which will hold all our overloading signatures:


type Signatures = {
    1: (x: string) => number,
    2: (x: string, y: number) => 42
    3: (x: number) => string
}

Now, in order to create overloadings, we need to obtain a union of all object properties and intersect them.


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

type Values<T> = T[keyof T]

type Overloading = UnionToIntersection<Values<Signatures>>

It works as expected:

declare const foo: Overloading;

const a = foo(123); // string
const b = foo('123'); // number
const c = foo('123', 456); // 42

Now, it is very easy to obtain a union of all allowed parameters:


//  [x: string] | [x: string, y: number] | [x: number]
type fooArgs = Parameters<Values<Signatures>>;
declare function bar(...args: fooArgs): string[]


const x = bar(123); // string[]
const y = bar('123'); // string[]
const z = bar('123', 456); // string[]

Whole code:



type Signatures = {
    1: (x: string) => number,
    2: (x: string, y: number) => 42
    3: (x: number) => string
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

type Values<T> = T[keyof T]

type Overloading = UnionToIntersection<Values<Signatures>>

declare const foo: Overloading;

const a = foo(123); // string
const b = foo('123'); // number
const c = foo('123', 456); // 42


//  [x: string] | [x: string, y: number] | [x: number]
type fooArgs = Parameters<Values<Signatures>>;
declare function bar(...args: fooArgs): string[]


const x = bar(123); // string[]
const y = bar('123'); // string[]
const z = bar('123', 456); // string[]

Playground

  • 1
    hah, this is awesome. I unfortunately don't control the source files in this case, so it'd still be re-hardcoding all of the signatures. Will keep this in mind in the future for other cases though! –  Nov 16 '21 at 16:27
  • Thanks. Please be aware that there is a drawback. It does not work very well if you have generics , like here https://stackoverflow.com/questions/65508351/is-it-possible-to-use-intersection-to-make-function-overloadings-with-generics – captain-yossarian from Ukraine Nov 16 '21 at 16:35
1

I found this question because I wanted somewhat the same result, but for a bunch of chainable write methods that I was too lazy for to write myself.

This is how I implemented the UnionToIntersection type from captain-yossarian's answer:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

// create accessor signature type. Generics are just for method params & return type.
type AccessorSignature<Self, In, Out = In> = {
  get(): Out;
  set(value: In): Self;
};

// apply union to intersection on accessor signature
type Accessor<Self, In, Out = In> = UnionToIntersection<
  AccessorSignature<Self, In, Out>[keyof AccessorSignature<Self, In, Out>]
>;

// create a class with the typed chainable accessors
declare class YeetProps {
  foo: Accessor<Yeet, string | number, string>;
  bar: Accessor<Yeet, string>;
  baz: Accessor<Yeet, boolean>;
}

// implement it in my final class
class Yeet extends YeetProps {}

const fooValue = new Yeet().foo(); // read string value

// chain write
new Yeet().foo("woop!").bar("this is amazing").baz(true);

I am eventually using a Proxy to actually implement the methods.