我正在尝试找出将类型分配给该通用类工厂的最佳方法。我从另一个问题中复制了部分代码:https : //stackoverflow.com/a/47933133将枚举值映射到类相对简单。但是,我似乎无法弄清楚如何进一步输入我的创建方法,以便它能识别出我正在创建的类实际上是否没有传入的参数。(我意识到这是一个(我想我已经将我在现实世界中的应用程序中尝试做的事情简化为这个问题)。
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());
请参阅上面的代码中的实际问题。
2020年5月29日更新:
@Kamil Szot指出,我的未重载函数首先没有适当的类型安全性:
// Should be an error but is not:
const cat = getAnimalInstance((() => Kind.DogKind)(), new CatParams());
因此,正如他所建议的,我们确实确实需要重载,但是我不想手动编写它们。所以,这就是我现在所拥有的。我认为这已经足够了,但是我希望我可以定义另一个类型,以使这些重载的自动生成变得不再那么冗长,并且使我不必重复执行两次函数实现的函数签名。
// 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;
另一个更简单的常规解决方案Playground链接
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;
type Tuples<T> = T extends Kind ? [T, InstanceType<KindMap[T]>, InstanceType<ParamsMap[T]>] : never;
type SingleKinds<K> = [K] extends (K extends Kind ? [K] : never) ? K : never;
type ClassType<A extends Kind> = Extract<Tuples<Kind>, [A, any, any]>[1];
type ParamsType<A extends Kind> = Extract<Tuples<Kind>, [A, any, any]>[2];
function getAnimalInstance<A extends Kind>(key:SingleKinds<A>, params: ParamsType<A>): ClassType<A> {
const animalKlass: ClassType<A> = kindMap[key];
const animalInstance = new animalKlass();
animalInstance.init(params);
return animalInstance;
}
// this works
const cat = getAnimalInstance(Kind.CatKind, new CatParams());
const shouldBeError = getAnimalInstance(Kind.DogKind, new CatParams()); // wrong params
const shouldBeErrorToo = getAnimalInstance(f(), new CatParams()); // undetermined kind
const shouldBeErrorAlso = getAnimalInstance(f(), new DogParams()); // undetermined kind
var k:Kind;
k = Kind.CatKind;
const suprisinglyACat = getAnimalInstance(k, new CatParams()); // even that works!
const shouldError = getAnimalInstance(k, new DogParams());
function f():Kind {
return Kind.DogKind;
}
这是另一个示例,用于反映我的其他需要手动重载的答案。它还可以自动获取Params类型,而无需单独的手动定义映射。
class DogParam { public n: number = 0; }
class CatParam { public n: string = "a"; }
class BatParam { public n: boolean = true; }
class Dog { init(p: DogParam) { } }
class Cat { init(p: CatParam) { } }
class Bat { init(p: BatParam) { } }
enum Kind { Dog, Cat, Bat }
const kindMap = {
[Kind.Dog]: Dog,
[Kind.Cat]: Cat,
[Kind.Bat]: Bat
}
type Tuples<K = Kind> = K extends Kind ? [
K,
InstanceType<(typeof kindMap)[K]>,
InstanceType<(typeof kindMap)[K]> extends
{ init: (a: infer P) => any } ? P : never
] : never;
type SingleKinds<K> = [K] extends (K extends Kind ? [K] : never) ? K : never;
type ClassType<K> = Extract<Tuples, [K, any, any]>[1];
type ParamsType<K> = Extract<Tuples, [K, any, any]>[2];
function a<K extends Kind>(k: SingleKinds<K>, p: ParamsType<K>): ClassType<K> {
var ins:ClassType<K> = new kindMap[k];
ins.init(p);
return ins;
}
function f(): Kind {
return Kind.Cat;
}
var k:Kind;
k = Kind.Cat;
a(Kind.Dog, new DogParam()); // works
a(Kind.Cat, new DogParam()); // error because mismatch
a(f(), new DogParam()); // error because kind undetermined
a(f(), new CatParam()); // error because kind undetermined
a(f() as Kind.Dog, new DogParam()); // works, but hey, it's your fault
// doing the wrong cast here manually
a(k, new CatParam()); // even this works
a(k, new DogParam()); // and this error
// you need to use exactly one kind at a time or it errors
var mixed: Kind.Dog | Kind.Cat = null as any;
var b = a(mixed, new DogParam());
var mixedfn = ():Kind.Dog | Kind.Cat => null as any;
var b = a(mixedfn(), new DogParam());
融合了我和Taytay想法的解决方案,该想法生成了从“种类到类”映射所需的一切,并使用自动生成的函数重载来提供良好的智能感知游乐场链接
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;
type Tuples<K = Kind> = K extends Kind ? [
K,
InstanceType<KindMap[K]>,
InstanceType<(typeof kindMap)[K]> extends
{ init: (a: infer P) => any } ? P : never
] : never;
type ClassType<K> = Extract<Tuples, [K, any, any]>[1];
type ParamsType<K> = Extract<Tuples, [K, any, any]>[2];
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type Fnc<T = Tuples> = UnionToIntersection<
T extends Tuples ? (key: T[0], p: T[2]) => T[1] : never
>;
var getAnimalInstance:Fnc = function<K extends Kind>(key: K, params:ParamsType<K>):ClassType<K> {
const animalKlass = kindMap[key];
const animalInstance = new animalKlass();
animalInstance.init(params);
return animalInstance;
}
// works
const cat = getAnimalInstance(Kind.CatKind, new CatParams());
// errors
const shouldBeError = getAnimalInstance((() => Kind.DogKind)(), new CatParams());
提出问题的用户Taytay在此处的Playground链接中对此代码进行了调查,以确定其工作方式。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句