TypeScript Generics: 'type is not assignable to type T'

Joe picture Joe · Apr 10, 2018 · Viewed 12.2k times · Source

I have made a factory that will create instances of certain classes. I want to use generics to ensure that all of the objects returned are from sub-classes that extend an abstract classes.

I thought that the logic of the createInstance method shown below could be described as 'createInstance() will return a type T that is constrained to be a class that extends Animal.

As you can see, Lion extends Animal, but I still get the compiler warning type Lion is not assignable to type T.

abstract class Animal {
    abstract makeSound(): void;
}

class Bear extends Animal {
    public makeSound() {
        console.log('growl');
    }
}

class Lion extends Animal {
    public makeSound() {
        console.log('roar');
    }
}

function createInstance<T extends Animal>(type: string): T {
    switch(type) {
        case 'bear':
            return new Bear(); // 'type Bear is not assignable to type T'
        case 'lion':
            return new Lion(); // 'type Lion is not assignable to type T'
    }
}

createInstance().makeSound();

I have read at the end of the TypeScript Generics docs that:

When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example,

function create<T>(c: {new(): T; }): T {
    return new c();
}

but I don't really want to have to pass in the class constructor into the function if possible and would like to understand why I'm getting the not assignable to type T message in the first place.

Thanks

Answer

Titian Cernicova-Dragomir picture Titian Cernicova-Dragomir · Apr 10, 2018

If your function always returns a Lion it's result type is not really generic. You could for example write create<Tiger>() and your function would still return a Lion. A true generic function would return a value that honors the generic parameter.

You could pass the constructor as an argument, as you discovered:

function create<T>(c: {new(): T; }): T {
    return new c();
}

Or you could make your function not generic, and have it return an Animal or a Lion. You could have more overloads, if you have logic based on argument values that determine the return type:

// Public signatures, we tie function parameter values to return value for specific types
function createInstance(type: "Lion"): Lion 
function createInstance(type: "Tiger"): Tiger 
// Private signature, not visible from outside
function createInstance(type: "Lion" | "Tiger"): Animal {
    if(type === "Lion") {
        return new Lion();
    }
    else if(type === "Tiger") {
        return new Tiger(); 
    }
}
let tiger = createInstance("Tiger"); // will be typed as Tiger
let lion = createInstance("Lion");// will be typed as Lion
let err = createInstance("Lama");// will be an error since the function does not know how to create a Lama