Home > Software engineering >  Return subset of union depending on class in generic function
Return subset of union depending on class in generic function

Time:05-07

This is very much like my other question, but this time using classes instead of plain objects.

class Error1 extends Error {
    constructor(message: string, public arg1: string) {
        super(message);
    }
}

class Error2 extends Error {
    constructor(message: string, public arg2: string) {
        super(message);
    }
}

type MyError = Error1 | Error2;

type Constructor<T> = new (...args: any) => T;

function filterErrors<E extends MyError>(errors: MyError[], classes?: Constructor<E>[]): E[] {
  if (!classes) return errors as E[];
  return errors.filter((error) => classes.some((klass) => error instanceof klass)) as E[];
}

const e1 = new Error1("e1", "whatever");
const e2 = new Error2("e2", "whatever");
const errors = [e1, e2]
const f1 = filterErrors(errors); // OK
const f2 = filterErrors(errors, [Error1]); // OK
const f3 = filterErrors(errors, [Error1, Error2]) // Error

Also available in Typescript playground.

The error is:

Types of construct signatures are incompatible.
  Type 'new (message: string, arg2: string) => Error2' is not assignable to type 'new (...args: any) => Error1'.
    Property 'arg1' is missing in type 'Error2' but required in type 'Error1'.(2419)

I can work around it by specifying the generic parameter in the call site:

const f3 = filterErrors<Error1 | Error2>(errors, [Error1, Error2])

But that's obviously not ideal.

PS. If I remove the extra constructor arguments in Error1 and Error2 (which I'm not willing to do in practice), then the error goes away, but f3 is typed as Error1[] -- I'd expect (Error1 | Error2)[]

CodePudding user response:

I'm going to take over from my alt now and elaborate on my proposed solution.

Frankly, I'd like to work with constructors over instance types because InstanceType is a built-in and Constructor is not, which makes this answer a little more lean and clear.

type MyError = InstanceType<MyErrorConstructor>;
type MyErrorConstructor = typeof Error1 | typeof Error2;

And I've done just that here. A union of all the error constructors, then a union of all the instance types.

Next we change the definition of filterErrors:

function filterErrors<
  Constructors extends MyErrorConstructor = MyErrorConstructor
>(errors: MyError[], classes?: Constructors[]): InstanceType<Constructors>[] {

errors can be any list of errors, which is what we want. But classes, if provided, narrow down the return type of the function. Otherwise it doesn't do any narrowing because it defaults to MyErrorConstructor.

Now you can call it without any problems:

const f1 = filterErrors(errors);                   // (Error1 | Error2)[]
const f2 = filterErrors(errors, [Error1]);         // Error1[]
const f3 = filterErrors(errors, [Error1, Error2]); // (Error1 | Error2)[] 

Unfortunately you will notice the two ugly casts I have used. At the moment I don't know any other solution that's cleaner and doesn't require casts of some sort.

Playground

  • Related