TypeScript: как объявить фабрику классов generi c, которая самым безопасным способом отображает перечисления в типы? - PullRequest
1 голос
/ 29 мая 2020

Я пытаюсь найти лучший способ присвоить типы этой общей фабрике классов c. Я скопировал часть этого кода из другого вопроса: { ссылка } Сопоставить значение перечисления с классом относительно просто. Однако я не могу понять, как сделать go еще один шаг и ввести свой метод создания, чтобы он понял, действительно ли класс, который я создаю, не принимает параметры, которые я передал. (Я понимаю, что это - запутанный и надуманный способ создания экземпляра. Я думаю, что я выделил кое-что, что я пытаюсь сделать в своем приложении в реальном мире, на этот вопрос.)

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());

Ссылка на игровую площадку

См. Актуальный вопрос в приведенном выше коде.


ОБНОВЛЕНИЕ 29 мая 2020 г .:

@ Kamil Szot указывает, что у меня в первую очередь нет надлежащей безопасности типов в моей неперегруженной функции:

    // Should be an error but is not:
    const cat = getAnimalInstance((() => Kind.DogKind)(), new CatParams());

Итак, нам действительно нужны перегрузки, как он предлагает, но я не хочу напишите их вручную. Итак, вот что у меня есть. Я думаю, что это настолько хорошо, насколько это возможно, но я sh я мог бы определить другой тип, который сделал бы автоматическое создание этих перегрузок менее подробным и сделал это так, чтобы мне не приходилось дублировать сигнатуру функции моего реализация функции дважды.

// 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 Link

Ответы [ 2 ]

1 голос
/ 30 мая 2020

Другое более простое общее решение Ссылка на игровую площадку

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());

Решение, объединяющее как my, так и Taytay идей, которые генерируют все необходимое, от карты «виды до классов» и используют автоматически сгенерированную перегрузку функций, чтобы обеспечить хороший intellisense Ссылка на игровую площадку

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());

Пользователь Тайтай , задавший вопрос, исследовал этот код здесь Ссылка на игровую площадку , чтобы определить, как он работает.

1 голос
/ 29 мая 2020

Два разных более общих решения можно увидеть в конце вопроса и в принятом ответе.

Я тоже оставляю этот ответ, потому что он содержит более читаемое и легкое для понимания решение, однако требует, чтобы вы вручную определяли перегрузку функции для каждого Kind.

Обсуждение

Попробуйте определить свои инициалы следующим образом:

public init<P extends DogParams>(params: P) { }
//..
public init<C extends CatParams>(params: C) { }

Это не должно сильно измениться, но теперь TypeScript даже не позволит вам делать какие-либо вызовы init() на animalInstance (типа Dog | Cat) вот так:

function f(): Dog | Cat {
    return new Dog();
}
const dc: Dog | Cat = f();
dc.init(new DogParams());
// ^ here is the error

потому что

This expression is not callable.   
Each member of the union type '(<P extends DogParams>(params: P) => void) | (<C extends CatParams>(params: C) => void)' has signatures, 
but none of those signatures are compatible with each other.(2349)

Или вы можете go еще проще и объявить вот так:

public init(params: string) { } // inside class Dog
//..
public init(params: number) { } // inside class Cat

и теперь здесь

const dc: Dog | Cat = f();
dc.init(5);

dc.init имеет подпись init(params: never): void, и вы тоже не можете его вызвать.


Я думаю, что единственный способ сделать вызов init типобезопасным способом - это если вы выполняете ручную проверку типа во время выполнения и выполняете отдельные ручные приведения и вызовы для каждого случая, например:

const dc: Dog | Cat = f();
if (dc instanceof Dog) {
    dc.init("5");
} else if(dc instanceof Cat) {
    dc.init(5);
} else {
   throw Exception("I should implement call to init() of "+dc); // this will alert you if you add new kind of animal but forget to add it here.

Если вы предпочитаете получать предупреждение во время компиляции о том, что вы забыли реализовать новый введите этот ручной фрагмент кода, вы можете добиться этого, используя Discrim inated Unions и проверка полноты , но вам понадобится компилятор, чтобы иметь возможность определить, был ли вызван init() или нет, например, возвращая что-то из init().

// .. inside class Dog
public kind: Kind = Kind.DogKind; 
public init(params: string) { return true; } 

// .. inside class Cat
public kind: Kind = Kind.CatKind;
public init(params: number) { return true; } 
// ..

const dc: Dog | Cat = f();
enum Kind {
    DogKind = 'DogKind',
    CatKind = 'CatKind',
//    HamsterKind = 'HamsterKind'  // after uncommenting this, compiler alerts that function below does not always return boolean, and you know that you should implement the call to init() for new Kind there
}
(():boolean => {
    switch (dc.kind) {
        case Kind.DogKind: return (dc as Dog).init("5");
        case Kind.CatKind: return (dc as Cat).init(5);
    }
})();

Решение

Лично я бы go примерно так:

class DogParam {
    public n: number = 0;
}
class CatParam {
    public n: string = "a";
}

class Dog {
    init(p: DogParam) { }
}
class Cat {
    init(p: CatParam) { }
}

enum Kind {
    Dog, Cat //, Hamster  // if you add new kind compiler will fail 
                          // inside function a(), while trying to 
                          // get kindMap[k], because key k is potentially not 
                          // present in kindMap, and when you add it to 
                          // kindMap you still need to add new overload for 
                          // function a() to be able to use new Kind in your 
                          // code so at no point compiler lets you forget to 
                          // add anything
}
const kindMap = {
    [Kind.Dog]: Dog,
    [Kind.Cat]: Cat
} 

// The only drawback of this solution is that you have to list those 
// overloads manually.
function a(k: Kind.Dog, p: DogParam): Dog;
function a(k: Kind.Cat, p: CatParam): Cat;
function a(k: Kind, p: any) { 
    var ins = new kindMap[k];
    ins.init(p as any); // safe because overloads ensure it can be called 
    return ins;         // just for matching params
}


function f(): Kind {
    return 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

Ссылка на игровую площадку

Дополнительным преимуществом этого решения является то, что оно не генерирует ненужный исполняемый код.

...