12

How can I achieve something similar to this pattern in typescript?

class A {
    Init(param1: number) {
        // some code
    }
}

class B extends A {
    Init(param1: number, param2: string) {
        // some more code
    }
}

The code snipped above appears like it should work, however on close inspection of How Typescript function overloading works it makes sense that an error is thrown:

TS2415: 'Class 'B' incorrectly extends base class 'A'. 
Types of property 'Init' are incompatible.

I know that constructor functions allow this behaviour, but I can't use constructors here as these objects are pooled for memory efficiency.

I could provide another definition of Init() in class A:

class A {
    Init(param1: number, param2: string): void;
    Init(param1: number) {
        // some code
    }
}

However this is less than ideal as now the base class needs to know about all of its derived classes.

A third option would be to rename the Init method in class B but that would not only be pretty ugly and confusing, but leave exposed the Init() method in the base class, which would cause difficult-to-detect bugs when the base class Init() is called by mistake.

Is there any way to implement this pattern that doesn't have the pitfalls of the aforementioned approaches?

Community
  • 1
  • 1
Daniel
  • 1,125
  • 2
  • 9
  • 21

2 Answers2

12

TypeScript complains about methods not being interchangeable: what would happen if you do the following?

let a:A = new A(); // a is of type A
a.Init(1)
a = new B(); // a is still of type A, even if it contains B inside
a.Init(1) // second parameter is missing for B, but totally valid for A, will it explode?

If you don't need them to be interchangeable, modify B's signature to comply with A's:

class B extends A {
    Init(param1: number, param2?: string) { // param 2 is optional
        // some more code
    }
}

However, you might find yourself in a situation where you need to create a class with totally different method signature:

class C extends A {
    Init(param1: string) { // param 1 is now string instead of number
        // some more code
    }
}

In this case, add a list of method signatures that satisfy both current class and base class calls.

class C extends A {
    Init(param1: number)
    Init(param1: string)
    Init(param1: number | string) { // param 1 is now of type number | string (you can also use <any>)
        if (typeof param1 === "string") { // param 1 is now guaranteed to be string
            // some more code
        }
    }
}

That way the A class doesn't have to know about any of the derived classes. As a trade-off, you need to specify a list of signatures that satisfies both base class and sub class method calls.

zlumer
  • 6,844
  • 1
  • 25
  • 25
  • Thanks for your response! I'd prefer not to have to do runtime type checking if I can avoid it though, Typescript manages to prevent that most everywhere else. – Daniel Jun 25 '16 at 07:32
  • Unfortunately, you can't go without runtime checks if your subclass' methods signatures do not match base class ones. Take a look at my first example: someone may be calling your subclass as if it is actually base clase – zlumer Jun 25 '16 at 07:35
  • Ah, all right, I think I understand now. Perhaps I will make both classes inherit their other common properties from an abstract base class. – Daniel Jun 25 '16 at 07:46
  • `a.Init(1)` should definitively call A's init method because that's how OOP works. Init signature with 1 argument is inherited by A and init signature with 2 arguments is proper from B. – Sebastian Aug 04 '20 at 04:40
1

For someone who wants to extend a type. Basing on zlumer's answer, and using Intersection types

interface ConsumerGroup {
  on(message: 'message'): void
  on(message: 'error'): void
}

interface ConsumerGroup2 {
  on(message: 'messageDecoded'): void;
  on(message: 'rawMessage'): void;
}

// Intersection types
type ConsumerGroupEx = ConsumerGroup2 & ConsumerGroup;

function newEvent(): ConsumerGroupEx {
  return "just for test" as unknown as ConsumerGroupEx;
}

const evt = newEvent();

evt.on('messageDecoded'); // ok
evt.on('message'); // ok
evt.on('error'); // ok
evt.on('notExist'); // compilation error

ninhjs.dev
  • 7,203
  • 1
  • 49
  • 35