2
const Err: ErrorConstructor = class Err extends Error {};

ErrorConstructor isn't correct here, but I'm not sure how to correctly define it. Typescript isn't being helpful here and if I don't define the type it just says it's "typeof Err"

Note that this is a very condensed version of what I'm running into, I know I don't have to define it in this exact example but this is a minimal repro.

  • 1
    _'if I don't define the type it just says it's "typeof Err"'_ - Which is correct (and why do you declare a variable with the exact same name as the class `Err`?) – Andreas Jul 14 '21 at 12:16
  • 1
    @Andreas -- Yes, the typeof the thing is the typeof the thing... Not helpful ;) This is just an example, I'm just trying to figure out the type. e.g. if I were to write `type mustExtendErrorClass = ....???` –  Jul 14 '21 at 12:18

2 Answers2

2

If I understand what you want to do correctly, ErrorConstructor is a template that you want each of your Error derivatives to follow.

In such case, I would either derive a class CustomError from Error, and then derive the other Err classes from CustomError, or I would implement an interface ICustomError as well as extending directly from Error.

Say that you want all your Err types to give information about recoverability:

option 1: two levels of inheritance

class CustomError extends Error {
    constructor (
        public message: string,
        public recoverable: boolean
    ) {
        super();
        this.name = 'customError';
    }
}

class ErrA extends CustomError {

    constructor (
        public message: string,
        public recoverable: boolean
    ) {
        super(message, recoverable);
        this.name = 'ErrA';
    }
};

option 2: extends + implements

interface ICustomError {
    recoverable: boolean;
}

class ErrB extends Error implements ICustomError {
    constructor (
        public message: string,
        public recoverable: boolean
    ) {
        super();
        this.name = 'ErrB';
    }
};

Now what the type of errA and errB in the following? It's ErrA and ErrB, duh.

const errA = new ErrA('a', true);
const errB = new ErrB('b', true);

But since TS is a structural type system, you don't really care how they were constructed and you can use either of the following functions to display the message of both errA and errB

function displayCustomError(error: CustomError) {
    return error.message;
}

function displayCustomError2(error: Error & ICustomError) {
    return error.message;
}

function displayError(error: Error) {
    return error.message;
}
geoffrey
  • 2,080
  • 9
  • 13
2

You are getting typeof Err because classes are special values in TypeScript. Like enums they can be used as a type and as a value.

Consider next example:

class A extends Error { }
class B extends Error { }

const a = (arg: A) => arg

a(new A()) // ok

a(A) // error

/////////////////////////////


const b = (arg: typeof A) => arg

b(new A()) // error

b(A) // ok

If you expect argument to be a type of class instance, use function a .

If you expect argument to be a type of class itself, use function b.

If you want to create MustExtendsSomeClass, I believe you can just use typeof AnyClass:

class Animal {
    tag = 'animal'
}

class Dog extends Animal { }
class Cat extends Animal { }
class Ant { }

type MustExtendsAnimal = typeof Animal;

const e = <T extends MustExtendsAnimal>(error: T) => e

e(Dog) // ok
e(Cat) // ok
e(Ant) // error

As you might have noticed, Ant is unassignable and it is expected behaviour.

But why it does not work with Error class ?

class A extends Error { }

type MustExtendsAnimal = typeof Error;

const e = <T extends MustExtendsAnimal>(error: T) => e

e(A) // error

Because it is built-in class with non standard type signature for classes:

interface Error {
    name: string;
    message: string;
    stack?: string;
}

interface ErrorConstructor {
    new(message?: string): Error;
    (message?: string): Error;
    readonly prototype: Error;
}

declare var Error: ErrorConstructor;

It can be used with new keyword new Error('custom error') and as a regular function Error(123)

I'd willing to bet that this question is relative to our problem. If you want to represent a class type which extends Error class, you should make sure that calling it without new will create Error class instance.

Smth like that:


const Err = function (this: Error, message?: string) {
    if (new.target) {
        // If this function was called with new keyword
        this.message = message || ''
        this.name = 'some name'

        // some inheritance logic
    }
    // some inheritance logic
    return new Error('custom error')
}

But this function does not work either:



const Err = function (this: Error, message?: string) {
    if (new.target) {
        // If this function was called with new keyword
        this.message = message || ''
        this.name = 'some name'

        // some inheritance logic
    }
    // some inheritance logic
    return new Error('custom error')
}

type MustExtendsAnimal = typeof Error;

const e = <T extends MustExtendsAnimal>(error: T) => e

e(Err) // error

because TS does not recognize this function as a constructor.

This is why you cant create such type for Error class.

I believe you can use this solution:


type Overload<T extends InstanceType<typeof Error>> = ((this: Error, message?: string | undefined) => T) & { new(message?: string | undefined): T }

class A extends Error { }

const Err = A as Overload<A>

type MustExtendsAnimal = typeof Error;

const e = <T extends MustExtendsAnimal>(error: T) => e

e(Err) // error