How to Do Method Overloading in Typescript

TypeScript function overloading

This may be because, when both functions are compiled to JavaScript, their signature is totally identical. As JavaScript doesn't have types, we end up creating two functions taking same number of arguments. So, TypeScript restricts us from creating such functions.

TypeScript supports overloading based on number of parameters, but the steps to be followed are a bit different if we compare to OO languages. In answer to another SO question, someone explained it with a nice example: Method overloading?.

Basically, what we are doing is, we are creating just one function and a number of declarations so that TypeScript doesn't give compile errors. When this code is compiled to JavaScript, the concrete function alone will be visible. As a JavaScript function can be called by passing multiple arguments, it just works.

How to do method overloading in TypeScript?

According to the specification, TypeScript does support method overloading, but it's quite awkward and includes a lot of manual work checking types of parameters. I think it's mostly because the closest you can get to method overloading in plain JavaScript includes that checking too and TypeScript tries to not modify actual method bodies to avoid any unnecessary runtime performance cost.

If I understand it correctly, you have to first write a method declaration for each of the overloads and then one method implementation that checks its arguments to decide which overload was called. The signature of the implementation has to be compatible with all of the overloads.

class TestClass {
someMethod(stringParameter: string): void;
someMethod(numberParameter: number, stringParameter: string): void;

someMethod(stringOrNumberParameter: any, stringParameter?: string): void {
if (stringOrNumberParameter && typeof stringOrNumberParameter == "number")
alert("Variant #2: numberParameter = " + stringOrNumberParameter + ", stringParameter = " + stringParameter);
else
alert("Variant #1: stringParameter = " + stringOrNumberParameter);
}
}

Typescript Method overloading for different type of parameters but same response

Problem

From TypeScript's documentation:

Overload Signatures and the Implementation Signature

This is a common source of confusion. Often people will write code like this and not understand why there is an error:

function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
^^^^
Expected 1 arguments, but got 0.

Again, the signature used to write the function body can’t be “seen” from the outside.

The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.

The implementation signature must also be compatible with the overload signatures. For example, these functions have errors because the implementation signature doesn’t match the overloads in a correct way:

function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
^^
This overload signature is not compatible with its implementation signature.

function fn(x: boolean) {}
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
^^
This overload signature is not compatible with its implementation signature.

function fn(x: string | number) {
return "oops";
}

– TypeScript documentation on overload and implementation signatures

In your case you've defined the following overload signature:

static async myMethod(model: FooModel): Promise<BarResult>

But the implementation signature has no overlap. The first argument in your implementation signature is string while the overload is FooModel while the second argument in the implementation signature is string while the overload is undefined.

static async myMethod(inputText: string, outputText: string): Promise<BarResult>{

Solution

Turn your current implementation signature into an overload and add an implementation signature which is compatible with both your overloads:

class Foo {
static async myMethod(model: FooModel): Promise<BarResult>;
static async myMethod(inputText: string, outputText: string): Promise<BarResult>;
static async myMethod(modelOrInputText: string | FooModel, outputText?: string): Promise<BarResult>{
//implementation;
return new BarResult(); //Different content based on the inputs
}
}

– TypeScript Playground

Function Overloading in TypeScript vs Using a Union Type

Function overloading maps a specific input type to a specific return type. With a union you just know that the return is one of the valid types, but you lose the association between the input and the output. This may or may not be a problem depending on how and where you use the function. But that's the difference.

Here's what it looks like with overloads. The last line of the signature isn't one of the overloads, it describes the types for the implementation. Sometimes you'll see any, but you can also use the union here.

function overloaded(a: string): string
function overloaded(a: number): number
function overloaded(a: any): any {
return a;
}

The different arguments return the specific type based on the overload that they match.

const oNum: number = overloaded(0);
const oStr: string = overloaded("");
const oBool = overloaded(true); //error

In our union, both input types just return the union, so we've lost the specificity.

function union(a: string | number): string | number {
return a;
}
const uNum: string | number = union(0);
const uStr: string | number = union("");
const uBool = union(true); //error

There is a third option which is typescript generics. This allows us to keep the specificity while accepting infinitely many types. The boolean example works now. We are telling typescript "look at the type of the argument a and call that T". Then we get this type variable T which we can use in the return type. Here we just returning the same type T directly, but there is a lot more than you can do with it.

function generic<T>(a: T): T {
return a;
}
const gNum: number = generic(0);
const gStr: string = generic("");
const gBool: boolean = generic(true);

Typescript Playground Link

TypeScript class method overloads not behaving the same as function overloads

1. why is this not working for the class example?

I think the issue is that the signature in the class isn't being treated as just an implementation signature like the third standalone function signature is, because the overloads are declared separately. So the class is augmenting those, adding a third public signature, in contrast to the function overloads where the third signature is not public, it's just the implementation signature.

You can fix it by not putting the overloads (just) in the interface declaration. Either don't use an interface:

class SomeClient {
sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void;
sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void;
sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
// do something
}
}

Playground example

...or do use an interface, but also repeat the overloads in the class construct so TypeScript knows that the third one is an implementation signature:

interface ISomeClient {
sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

class SomeClient implements ISomeClient {
sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
// do something
}
}

Playground example

That's repetitive, but I'm not sure there's a way around it other than assigning to SomeClient.prototype after-the-fact.

2. is there some better way to achieve this...

I tend to like function overloads for this, but it's true that they don't work for everything and if you have lots of these, it'll get unwieldy fast.

I should note that I'm still just at the journeyman level with TypeScript so there may be other options, but I can think of two alternatives:

  1. Using a rest argument with a varying tuple type

  2. Using a discriminated union so there's always only one parameter

In places I've found function overloads to be too cumbersome, I've tended toward discriminated unions, but the tuple idea was kind of cute so I thought I'd include it.

Rest + Tuple

Instead of MessagePayload<T>, you have MessageParams<T> defining a tuple based on T:

type MessageParams<T extends MessageType> = T extends MessageType.FOO 
? [T, string]
: T extends MessageType.BAR
? [T, number]
: T extends MessageType.BAZ
? [T, User]
: [T];

(If you need MessagePayload<T> for other reasons, you can derive it from the above: type MessagePayload2<T extends MessageType> = MessageParams<T>[1];.)

Then the method uses that as the type of a rest parameter:

class SomeClient {
sendMessage<T extends MessageType>(...args: MessageParams<T>) {
const action = args[0];
// do something
}
}

Playground example

The developer experience is very much like overloads, though.

Discriminated Union

This last option is a bigger change: You don't have separate parameters at all, just a single object type that's a discriminated union:

type FOOMessage = {action: MessageType.FOO; payload: string;};
type BARMessage = {action: MessageType.BAR; payload: number;};
type BAZMessage = {action: MessageType.BAZ; payload: User;};
type OtherMessage = {action: Exclude<MessageType, MessageType.FOO | MessageType.BAR | MessageType.BAZ>;};
// `OtherMessage` is the catch-all for all message types other than the
// ones with their own interface, note the use of `Exclude`

type Message = FOOMessage | BARMessage | BAZMessage | OtherMessage;

// ...

class SomeClient {
sendMessage(message: Message) {
const action = message.action;
// do something
}
}

The calls to it change to passing an object:

// tests
client.sendMessage({action: MessageType.FOO, payload: "string"});
client.sendMessage({action: MessageType.FOO}); // Error as desired
client.sendMessage({action: MessageType.QAT});

Playground example

Method overloading with an abstract base class

In TypeScript (and JavaScript) class methods are resolved by name, the parameters and their types are not involved. It works the same way as resolving any other property it's just that the value is a function and not a primitive type.

You can't actually have different implementations of a method that take different parameters. Overloading in TypeScript is simply a way for you to define valid combinations of types your function will accept which is why all but one of the function definitions are empty. There can only ever be one method called test and that same implementation will be called regardless of what arguments you supply.

You can replace the implementation of test in a derived class but again, that becomes the one implementation, the runtime won't pick the derived or the base implementation depending on which arguments you pass. If you call c.test() you will always get the implementation defined in C.

Because B is defining some overloads for test, when you provide the implementation in C you have to also satisfy the overloads of B because the C implementation entirely replaces the one in B and will have to be able to cope with all the parameter variants B.test wants as well and that is what the error message is telling you.

If you change C to this so that its test matches the one in B then the compiler is happy...

class C extends B {
public test(sOrN?: string | number | object): unknown {
return undefined;
}
}

TypeScript function overloading. IDE argument type confusion

This is a common problem - for instance in event listener contexts - and I faced it myself recently. Fortunately, there is a simple and elegant solution.

First of all, in your question there are two slightly different formulations of the problem; once, the object you want to type correctly is another argument to a, and once it is the singular argument to a function that you want to return from a. The following solution should work for both versions, but I'll present it for the first scenario, which should be more common.

The main trick is to avoid overloads and replace them by a single generic; that also makes handling a and typing much easier. As far as I can think, this should always be possible in the cases you describe.

Step 1: Define your function signatures/overloads by an interface:

interface Signatures {
key1: {arg1: number};
key2: {arg2: number};
}

Of course, that would be a bit more difficult for more than 2 arguments; but scenarios that are typical for the mentioned problem to arise usually have exactly 2 arguments - one key and one value.

Step 2: Define a single function generic function signature:

function a<K extends keyof Signatures>(key: K, args: Signatures[K]): string {
return 'hello world'; // TODO
}

Then, when you call a('key1', ), args is automatically inferred to be of type {arg1: number}.



Related Topics



Leave a reply



Submit