I am trying to figure out the best way to assign types to this generic class factory. I've copied some of this code from another question: https://stackoverflow.com/a/47933133 It's relatively straightforward to map an enum value to a class. I can't however seem to figure out how to go one step further and type my creation method so that it realizes if the class I am creating does not in fact take the parameters that I've passed in. (I realize this is a convoluted and contrived way to construct an instance. I think I've distilled down something I'm trying to do in my app in the real world to this question though.)
class Dog {
public dogName: string = ""
public init(params: DogParams) { }
}
class Cat {
public catName: string = ""
public init(params: CatParams) { }
}
class DogParams { public dogValues: number = 0 }
class CatParams { public catValue: number = 0}
enum Kind {
DogKind = 'DogKind',
CatKind = 'CatKind',
}
const kindMap = {
[Kind.DogKind]: Dog,
[Kind.CatKind]: Cat,
};
type KindMap = typeof kindMap;
const paramsMap = {
[Kind.DogKind]: DogParams,
[Kind.CatKind]: CatParams,
}
type ParamsMap = typeof paramsMap;
function getAnimalClasses<K extends Kind>(key: K, params: ParamsMap[K]): [KindMap[K], ParamsMap[K]] {
const klass = kindMap[key];
return [klass, params];
}
// Cool: Typescript knows that dogStuff is of type [typeof Dog, typeof DogParams]
const dogStuff = getAnimalClasses(Kind.DogKind, DogParams);
// Now imagine I want to instantiate and init my class in a type-safe way:
function getAnimalInstance<K extends Kind>(key: K, params: InstanceType<ParamsMap[K]>): InstanceType<KindMap[K]> {
const animalKlass = kindMap[key];
// animalInstance : Dog | Cat
const animalInstance = new animalKlass() as InstanceType<KindMap[K]>;
// By this line, Typescript just knows that animalInstance has a method called init that takes `DogParams & CatParams`. That makes sense to me, but it's not what I want.
// QUESTION: The following gives an error. Is there a type-safe way that I can make this method call and ensure that my maps and my `init` method signatures are
// are consistent throughout my app? Do I need more generic parameters of this function?
animalInstance.init(params);
return animalInstance;
}
// This works too: It knows that I have to pass in CatParams if I am passing in CatKind
// It also knows that `cat` is an instance of the `Cat` class.
const cat = getAnimalInstance(Kind.CatKind, new CatParams());
See the actual question in the code above.
UPDATE May 29, 2020:
@Kamil Szot points out that I don't have proper type safety in my non-overloaded function in the first place:
// Should be an error but is not:
const cat = getAnimalInstance((() => Kind.DogKind)(), new CatParams());
So, we really do need overloads, as he suggests, but I don't want to write them manually. So, here's what I've got now. I think that this is as good as it's going to get, but I wish I could define another type that made auto-generating these overloads less verbose and made it so that I didn't have to duplicate the function signature of my function implementation twice.
// We can use UnionToIntersection to auto-generate our overloads
// Learned most of this technique here: https://stackoverflow.com/a/53173508/544130
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
const autoOverloadedCreator: UnionToIntersection<
Kind extends infer K ?
K extends Kind ?
// I wish there was a way not to have to repeat the signature of getAnimalInstance here though!
(key: K, p: InstanceType<ParamsMap[K]>) => InstanceType<KindMap[K]> :
never : never
> = getAnimalInstance;
// This works, and has overload intellisense!
let cat2 = autoOverloadedCreator(Kind.CatKind, new CatParams());
// And this properly gives an error
const yayThisIsAnErrorAlso = autoOverloadedCreator((() => Kind.DogKind)(), new CatParams());
// Note that this type is different from our ManuallyOverloadedFuncType though:
// type createFuncType = ((key: Kind.DogKind, p: DogParams) => Dog) & ((key: Kind.CatKind, p: CatParams) => Cat)
type CreateFuncType = typeof autoOverloadedCreator;