2

Is it possible to overload whole class with a type definition?

For example, there are two classes that are different only in that if one of them has a generic typedef, it should also accept it in constructor parameter. If not, there is no need for that param and custom constructor.

export abstract class MyClass implements OtherClass {
  readonly foo;

  static get bar(): string {
    return "bar";
  }
}

export abstract class MyPayloadClass<T> extends MyClass {
  constructor(public readonly payload: T) {
    super();
  }
}

The goal is to merge these two together. So we only have a MyClass and it optionally has a constructor with payload only if the type <T> is provided.

3 Answers 3

4

While you can't have a single implementation for both classes, you can have a single constructor for both, and in effect have a unitary way of creating either the generic or non generic version.

What we do, is declare a variable that will hold the payload version of the class constructor and overload the constructor with the non payload version. The runtime implementation will always be the payload version, but you get type checking for the non payload version if there is no payload.

export abstract class MyClass  {
  readonly foo;

  static get bar(): string {
    return "bar";
  }
}

export abstract class MyPayloadClass<T> extends MyClass {
  constructor(public readonly payload: T) {
    super();
  }
}



const Both: typeof MyPayloadClass & { new() : MyClass } = MyPayloadClass as any;

let a = new Both();
let b = new Both(1);

b.payload // ok
a.payload // error

Both.bar // statics work
Sign up to request clarification or add additional context in comments.

1 Comment

Chef's kiss - just used this myself.
2

You could use a default generic, but it would require always passing an argument to the constructor, even if it is only undefined.

export class MyPayloadClass<T = void> extends MyClass {
  constructor(public readonly payload: T) {
    super();
  }
}


const c = new MyPayloadClass() // doesn't work
const c2 = new MyPayloadClass(undefined)

Or, you could create a static factory method with type overloads...

export class MyPayloadClass<T> extends MyClass {
  constructor(public readonly payload: T) {
    super();
  }

  public static factory(): MyPayloadClass<void>
  public static factory<T>(payload: T): MyPayloadClass<T>
  public static factory(payload?: any) {
    return new MyPayloadClass(payload)
  }
}

But this won't work with abstract classes. Get around this by using the reflect api.

export abstract class MyPayloadClass<T> extends MyClass {
  constructor(public readonly payload: T) {
    super();
  }

  public static factory(): MyPayloadClass<void>
  public static factory<T>(payload: T): MyPayloadClass<T>
  public static factory(payload?: any) {
    return Reflect.construct(this, [payload])
  }
}

Comments

0

For anyone seeing this in the future, you could also use tuples to conditionally omit the parameter in the constructor:

class MyPayloadClass<T = undefined> {
  public readonly payload: T;

  constructor(...args: T extends undefined ? [] : [T]) {
    this.payload = args[0] as T;
  }
}

const a = new MyPayloadClass();
const b = new MyPayloadClass(1);

a.payload; // undefined
b.payload; // number

And if you want to make the parameter optional instead of omitting it, you could use labeled tuples (since TypeScript 4.0):

abstract class MyPayloadClass<T = undefined> {
  public readonly payload: T;

  constructor(
    ...args: T extends undefined
      ? [opts?: { description?: string }] // Here's the trick, you can make a labeled tuple element optional
      : [opts: { payload: T; description?: string }]
  ) {
    this.payload = args[0] as T;
  }
}

class NoPayload extends MyPayloadClass {}

class StringPayload extends MyPayloadClass<string> {}

class ObjectPayload<
  T extends Record<string, unknown>,
> extends MyPayloadClass<T> {}

const a = new NoPayload();
const b = new NoPayload({ description: "useful information" });
const c = new StringPayload({ payload: "some data" });
const d = new ObjectPayload({ payload: { some: "data" } });
const e = new ObjectPayload(); // Expected 1 arguments, but got 0.
const f = new ObjectPayload({ description: "useful information" }); // Property 'payload' is missing

a.payload; // undefined
b.payload; // undefined
c.payload; // string
d.payload; // { some: string }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.