Home > other >  Typescript initialize generic error handling class extending Error
Typescript initialize generic error handling class extending Error

Time:08-30

Say that I have declared my classes and types as below

declare type Class<T = any> = new (...args: any[]) => T

export class BaseIntersectHandler<UuidException extends Class<Error>>
{
    constructor(private uuid_exception: UuidException) { 
         // implementation
    }
    debugMethod(): void {
        throw new this.uuid_exception("debug string")
    }
}

export class ClickHandlerUuidException extends Error
{
    // implementation
}

export class ClickHandler extends BaseIntersectHandler<ClickHandlerUuidException>
{ 
     constructor() {
          super(ClickHandlerUuidException)
     }
}

const sampleInstance = new ClickHander()
sampleInstance.debugMethod() // should throw the error from base method

Implementing above gives the error below when compiling:

      TS2344: Type 'ClickHandlerUuidException' does not satisfy the constraint 'Class<Error>'.
  Type 'ClickHandlerUuidException' provides no match for the signature 'new (...args: any[]): Error'.

I'm trying to be specific about UuidException generic being inherited from Error in JS. How should I change this? I'm sure this is a duplicate, but any help is appreciated!

CodePudding user response:

You most obviously violate the LSP: your base class has a constructor that can take any number of arguments, but your derived class has a constructor that accepts exactly no arguments. The compiler duly complains.

There are two obvious ways out.

The traditional OOP way is a protected constructor and a factory method.

class Base {
  protected constructor(...args: any[]) { ... }  // Not publicly visible.
  // NOTE: this class is an abstract base, so it cannot be instantiated.
}

class Derived extends Base {
  // The signature of the constructor is the same, but nobody can misuse the args.
  protected constructor(...args: any[]) { 
    super("Whatever", "It", "Needs");
    // Some more initialization. 
  }
  create() { return new Derived(); }  // Can call the constructor.
}

class AlsoDerived extends Base {
  // Or use the inherited base constructor directly.
  create(color: string) { return new AlsoDerived("Color", color); }
}

// This works:
Derived.create();
AlsoDerived.create("red");

// Won't work:
new Derived();

A better way would be to define an interface, and avoid inheritance.

Since a constructor is not a part of the interface, it can have any signature a particular class needs.

interface Debuggable {
  debugMethod(): void;
}

class A implements Debuggable {
  constructor(a: number, b: string) { ... }
  debugMethod() { console.log("This is A"); }
}

class B implements Debuggable {
  constructor() { ... }
  debugMethod() { console.log("This is B"); }
}

But these classes cannot inherit from one another. Some traditional OOP approaches rely heavily on having large trees of derived classes, and on using a few classes closest to the root for type declaration. This usually can be replaced by defining several interfaces, and making particular classes implement some of them, often more than one.

This approach avoids typical bugs (forgot to override a method, the inherited method wreaks havoc) and gives much more flexibility. It takes some getting used to, though.

CodePudding user response:

I fixed the above issue by removing the type Class and work directly with Error as below:

export class BaseIntersectHandler<UuidException extends Error>
{
    constructor(private uuid_exception: { new(...args: any[]): UuidException } ) { 
         // implementation
    }
    debugMethod(): void {
        throw new this.uuid_exception("debug string")
    }
}
  • Related