Typescript : 가장 안전한 방법으로 열거 형을 유형에 매핑하는 제네릭 클래스 팩토리를 어떻게 선언합니까?

Taytay

이 일반 클래스 팩토리에 유형을 할당하는 가장 좋은 방법을 찾으려고합니다. 이 코드의 일부를 다른 질문에서 복사했습니다. https://stackoverflow.com/a/47933133 enum 값을 클래스에 매핑하는 것은 비교적 간단합니다. 그러나 한 단계 더 나아가서 생성 방법을 입력하는 방법을 알아낼 수없는 것 같습니다. 생성하는 클래스가 실제로 전달한 매개 변수를 사용하지 않는지 확인합니다. 인스턴스를 구성하는 복잡하고 인위적인 방법입니다.하지만 실제 세계에서 내 앱에서하려는 작업을이 질문에 추출했다고 생각합니다.)

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;

놀이터 링크

카밀 조트

더 간단한 또 ​​다른 일반 솔루션 플레이 그라운드 링크

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] 삭제

에서 수정
0

몇 마디 만하겠습니다

0리뷰
로그인참여 후 검토

관련 기사

Related 관련 기사

뜨겁다태그

보관