Reputation: 138
I have a method that has 2 arguments and I want it to infer a type from the 1st argument.
For example, in the following code, I want the type T
of the function create_C<T>
to be inferred from the firstArgument
so that the return type of create_C
function would be C<type inferred from firstArgument>
interface C<T> {
firstArgument: A<T>;
secondArgument: (obj: any) => T
}
export interface A<T> {
type: T;
}
function create_C<T>(
firstArgument: A<T>,
secondArgument: (obj: any) => T
): C<T> {
return {
firstArgument,
secondArgument
}
}
However, in the following implementation, the type of const c
is being inferred as C<{ prop2: number }>
. But I am expecting it to be inferred as C<B>
and I am expecting the compiler to throw an error saying that the return type of the secondArgument
is not of type B
interface B {
prop1: string;
prop2: number
}
export class B_Component implements A<B> {
type: B = {
prop1: "",
prop2: 1
};
}
const c = create_C(
new B_Component(),
() => ({ prop2: 2 })
)
How can I make sure for the compiler to throw an error saying that the return type of the secondArgument
is not of type B
?
Here is a Stackblitz editor link: https://stackblitz.com/edit/qqddsn
Upvotes: 9
Views: 6258
Reputation: 327624
In your function signature
declare function create_C<T>(a1: A<T>, a2: (obj: any) => T): C<T>;
there are two inference sites for T
(an "inference site" means "someplace the compiler can use to try to infer a type for a type parameter"). One site is from the type
property of the first argument a1
, and the other site is the return type of the second argument a2
. The compiler looks at a call like
create_C(new B_Component(), () => ({ prop2: 2 });
and tries to infer T
from both sites. In this case, there is a match: both (new B_Component()).type
and {prop2: 2}
are assignable to {prop2: number}
. So there's no error, and you get C<{prop2: number>
coming out. In another situation, this might be exactly the behavior you want from the compiler.
Instead, you want to see the compiler use just a1
to infer T
, and to just verify that a2
matches it. That is, you want the T
in (obj: any) => T
to be a non-inferential type parameter (see microsoft/TypeScript#14829). Unfortunately, there is no "official" support for this. But fortunately, there are workaround techniques which can often be used to get this behavior.
Here's one such technique: if you change a type parameter in an inference site from T
to T & {}
, it lowers the site's priority. So the compiler will tend to infer T
from other inference sites first and only come back to the T & {}
one if it fails to infer from other places. And the type T & {}
is very similar to T
(if T
is an object type then it's basically the same) so it doesn't change the semantics much. Let's try it:
declare function create_C_better<T>(a: A<T>, b: (obj: any) => T & {}): C<T>;
Here goes:
const c2 = create_C_better(
new B_Component(),
() => ({ prop2: 2 }) // error!
// ~~~~~~~~~~~~~~ <-- prop1 is missing
)
const c3 = create_C_better(
new B_Component(),
() => ({ prop1: "all right", prop2: 2 })
); // C<B>
There, you get the error you wanted when prop1
is missing, and when you fix it, you get an output of type C<B>
as desired.
Okay, hope that helps; good luck!
Upvotes: 12
Reputation: 1227
It is due to the secondArgument: (obj: any) => T
.
If you apply above definition to () => ({ prop2: 2 })
Type of T
is { prop2: number }
. You may change it to something else to get the desired result.
For example.
interface C<T> {
firstArgument: A<T>;
secondArgument: (obj: any) => any;
}
export interface A<T> {
type: T;
}
declare function create_C<T>(
firstArgument: A<T>,
secondArgument: (obj: any) => any
): C<T>;
interface B {
prop1: string;
prop2: number;
}
export class B_Component implements A<B> {
type: B;
configuration: B = {
prop1: "",
prop2: 1
};
}
const b = new B_Component();
export const c = create_C(b, () => ({ prop2: 2 }));
Upvotes: -1