I have stumbled upon a peculiar problem, and I have no idea how to fix this. I have a class with a generic type. This class contains a method with only one union parameters asking for the generic type or an object. Since this class is instantiated multiple times with different types, I wanted to create a generic method that retrieves one of those instances (generically typed), and make a call of the method within.
Here's a code for reproduction:
export interface A {
a: any;
}
export interface B {
b: any;
}
export interface MyClasses {
a: MyClass<A>;
b: MyClass<B>;
}
export interface C {
c: any;
}
export declare class MyClass<T = { [prop: string]: any }> {
myMethod(data: T | C): T;
}
export type MyClassesKeys = keyof MyClasses;
export type MyClassInferGenericType<T> = T extends MyClass<infer G> ? G : T;
export class MyService {
private myClasses: MyClasses = {
a: new MyClass<A>(),
b: new MyClass<B>()
};
public getMyClass<T extends MyClassesKeys>(keyName: T): MyClasses[T] {
return this.myClasses[keyName];
}
public callerMethod<T extends MyClassesKeys, U extends MyClassInferGenericType<MyClasses[T]>>(key: T, myData: U) {
// This part is usually retrieved gener
const myClassInstance = this.getMyClass(key);
// My call
myClassInstance.myMethod(myData);
}
}
This returns, somehow, a compilation error:
Argument of type 'A | B' is not assignable to parameter of type '(A | C) & (B | C)'.
Type 'A' is not assignable to type '(A | C) & (B | C)'.
Type 'A' is not assignable to type 'A & C'.
Property 'c' is missing in type 'A' but required in type 'C'.
With "myClassInstance" being of type MyClasses[T] and myData taking the generic type associated with MyClasses[T], everything should be correctly inferred.
So, why is typescript trying to do an intersection of my types?
CodePudding user response:
The reason you get an intersection is because the compiler loses track of the correlation between the type of key
and the type of myData
, and so it only accepts something it knows is safe. If myData
were both A
and B
(like {a: 1, b: 2}
), then it would be safe to call myClassInstance.myMethod(myData)
no matter which key
were passed in.
That's what happens because myClassInstance.myMethod
is seen having a union type:
const x = myClassInstance.myMethod;
// const x: ((data: A | C) => A) | ((data: B | C) => B)
And if you call a union of functions, you have to pass in an intersection of its parameters for the compiler to accept it.
Of course, it's impossible for someone to pass in myData
and key
that are not properly matched. (Actually that's not true, since key
could be of a union type. Without ms/TS#27808 it is possible. Let's say that it's possible but unlikely. Or at least that we won't worry about it here.) But the compiler can't see this.
There isn't really support for the compiler to see arbitrarily complex correlations between different types. The basic issue is described in microsoft/TypeScript#30581 which frames it in terms of correlated unions.
Luckily, there is often a way to refactor the types in a way that the compiler is able to follow the logic. This is described in microsoft/TypeScript#47109. The idea is to make a "basic" type that represents your input/output relationship as simply as possible:
interface MyClassGenerics {
a: A;
b: B
}
And then we write MyClasses
explicitly as a mapped type over that basic type:
type MyClasses = {
[K in keyof MyClassGenerics]: MyClass<MyClassGenerics[K]>
}
So now, when you write MyClasses[K]
, the compiler will automatically see its relationship to MyClass<MyClassGenerics[K]>
. And you can refer to MyClassGenerics[K]
without needing to use MyClassInferGenericType
:
public callerMethod<K extends MyClassesKeys>(key: K, myData: MyClassGenerics[K]) {
const myClassInstance = this.getMyClass(key);
const x = myClassInstance.myMethod
// const x: (data: C | MyClassGenerics[K]) => MyClassGenerics[K]
myClassInstance.myMethod(myData); // okay
}
Now the type of myClassInstance.myMethod
is seen to be a single function type whose input and output types depend on the generic type parameter K
. It's no longer a union, so it can be called more easily.