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.