148

How can I declare a class type, so that I ensure the object is a constructor of a general class?

In the following example, I want to know which type should I give to AnimalClass so that it could either be Penguin or Lion:

class Animal {
    constructor() {
        console.log("Animal");
    }
}

class Penguin extends Animal {
    constructor() {
        super();
        console.log("Penguin");
    }
}

class Lion extends Animal {
    constructor() {
        super();
        console.log("Lion");
    }
}

class Zoo {
    AnimalClass: class // AnimalClass could be 'Lion' or 'Penguin'

    constructor(AnimalClass: class) {
        this.AnimalClass = AnimalClass
        let Hector = new AnimalClass();
    }
}

Of course, the class type does not work, and it would be too general anyway.

5 Answers 5

94

Edit: This question was answered in 2016 and is kind of outdated. Look at @Nenad up-to-date answer below.

Solution from typescript interfaces reference:

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

So the previous example becomes:

interface AnimalConstructor {
    new (): Animal;
}

class Animal {
    constructor() {
        console.log("Animal");
    }
}

class Penguin extends Animal {
    constructor() {
        super();
        console.log("Penguin");
    }
}

class Lion extends Animal {
    constructor() {
        super();
        console.log("Lion");
    }
}

class Zoo {
    AnimalClass: AnimalConstructor // AnimalClass can be 'Lion' or 'Penguin'
    
    constructor(AnimalClass: AnimalConstructor) {
        this.AnimalClass = AnimalClass
        let Hector = new AnimalClass();
    }
}
Sign up to request clarification or add additional context in comments.

10 Comments

I just wanted to add that this solution did NOT work for me, and will not work if you want to use a constructor which takes a non-zero number of arguments. Took me forever to figure out why this example works but my code didn't. The thing I found after hours of searching was to use AnimalClass: typeof Animal. This will work for dynamically loading subclasses of a given class.
@pixelpax You can define a non-zero argument constructor like this: new (...args: any[]): Animal
@Sammi, Thank you, your solution does make sense. For me, using typeof Animal to mean "the type of any subclass of Animal" worked really well, and looks descriptive in code.
@pixelpax Its meaningless though, as typeof Animal is just the string "function". Any class will satisfy that, not just Animal ctors.
Its meaningless though, as typeof Animal is just the string "function" -- in plain JS, yes. In TypeScript type annotation, no. typeof X means the type of the symbol X, whatever it is, and is being processed by the compiler statically.
|
86

I am not sure if this was possible in TypeScript when the question was originally asked, but my preferred solution is with generics:

class Zoo<T extends Animal> {
    constructor(public readonly AnimalClass: new () => T) {
    }
}

This way variables penguin and lion infer concrete type Penguin or Lion even in the TypeScript intellisense.

const penguinZoo = new Zoo(Penguin);
const penguin = new penguinZoo.AnimalClass(); // `penguin` is of `Penguin` type.

const lionZoo = new Zoo(Lion);
const lion = new lionZoo.AnimalClass(); // `lion` is `Lion` type.

11 Comments

+1 This answer is what the official documentation recommends.
You might need to specify the arguments too: new (...args: unknown[]) => T
@Nenad: If I change the Zoo constructor to take (public readonly AnimalClass: new () => Lion) or (public readonly AnimalClass: new () => Penguin), it works fine and both Lion and Penguin instances are instantiated as expected. Shouldn't only the type passed to the constructor work? What gives?
@atlantis This is interesting. You can also write const constructor: new () => Lion = Penguin;, but not const constructor: new () => number = Penguin;, so it seems TS is checking return type, but somehow does not differentiate two different subclasses. Maybe a bug?
How if AnimalClass references Zoo class in it's constructor?
|
54

Like that:

class Zoo {
    AnimalClass: typeof Animal;

    constructor(AnimalClass: typeof Animal ) {
        this.AnimalClass = AnimalClass
        let Hector = new AnimalClass();
    }
}

Or just:

class Zoo {
    constructor(public AnimalClass: typeof Animal ) {
        let Hector = new AnimalClass();
    }
}

typeof Class is the type of the class constructor. It's preferable to the custom constructor type declaration because it processes static class members properly.

Here's the relevant part of TypeScript docs. Search for the typeof. As a part of a TypeScript type annotation, it means "give me the type of the symbol called Animal" which is the type of the class constructor function in our case.

6 Comments

typeof returns a string, you can't use the result to call new
typeof Animal is a TypeScript type annotation which "returns" the type of the Animal constructor function. It doesn't return a string.
It's always a good idea to run the code in question in typescript playground and consult the docs (typescriptlang.org/docs/handbook/classes.html) before trying to correct the answer. Never hurts :).
Way better and more intuitive than the accepted answer!
It does not work with an abstract class though.
|
16

How can I declare a class type, so that I ensure the object is a constructor of a general class?

A Constructor type could be defined as:

 type AConstructorTypeOf<T> = new (...args:any[]) => T;

 class A { ... }

 function factory(Ctor: AConstructorTypeOf<A>){
   return new Ctor();
 }

const aInstance = factory(A);

1 Comment

Unfortunately with this method you lose type checking of the constructor arguments.
2

Use typeof to refer a Class Constructor

class Animal {
    constructor() {
        console.log("Animal");
    }
}

class Penguin extends Animal {
    constructor() {
        super();
        console.log("Penguin");
    }
}

class Lion extends Animal {
    constructor() {
        super();
        console.log("Lion");
    }
}

class Zoo {
    AnimalClass: typeof Animal // AnimalClass could be 'Lion' or 'Penguin'

    constructor(AnimalClass: typeof Animal) {
        this.AnimalClass = AnimalClass
        let Hector = new AnimalClass();
    }
}

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.