Home > database >  TypeScript class method overloads not behaving the same as function overloads
TypeScript class method overloads not behaving the same as function overloads

Time:04-07

Apologies in advance if this is a duplicate, but my search has not turned up anything that quite fits the issue I'm having.

Firstly, the desired behavior is to have a class method with two params, the second optional, where the second param's type is dependent on the first param's type. If the first param is type A, the second param should always be required and should be of type X, if the first param is type B, the second param should be omitted.

I've achieved something quite like this with function overloading:

// types
enum MessageType { FOO, BAR, BAZ }

type MessagePayload<T extends MessageType> = T extends MessageType.FOO 
    ? string
    : T extends MessageType.BAR
    ? number
    : never;

// overloads
function sendMessage<T extends MessageType.BAZ>(action: T): void

function sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void

// implementation
function sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
  // do something
}

// tests
sendMessage(MessageType.FOO, "10") // no error - as expected
sendMessage(MessageType.FOO, 10)   // error - as expected, payload is not string
sendMessage(MessageType.FOO)       // error - as expected, payload must be string
sendMessage(MessageType.BAZ);      // no error - as expected - since MessageType is BAZ

However, the same exact constructs, when applied to a class method, do not produce the same results. This snippet is a continuation of the first and uses the same types:

// interface
interface ISomeClient {
  sendMessage<T extends MessageType.BAZ>(action: T): void

  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

// implementation
class SomeClient implements ISomeClient {
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

// tests
const client = new SomeClient();

client.sendMessage(MessageType.FOO, "10"); // no error - as expected
client.sendMessage(MessageType.FOO, 10);   // error, payload is not string
client.sendMessage(MessageType.FOO)        // no error??? different behavior than function example
client.sendMessage(MessageType.BAZ);       // this part works fine

Here is a more complete example on the TS Playgound.

So, I guess this is a two-parter:

  1. why is this not working for the class example?
  2. is there some better way to achieve this that will work for both classes and functions and that doesn't require maintaining an overload to capture the types that don't require a payload? I've used an enum and a conditional type here to constrain the second param to match what's expected given the first param. I've played around with another way involving an key to type map but it seems hacky, still requires overloads, and suffers from this same issue for classes and functions.

Thanks.

CodePudding user response:

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 link

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

  • Related