Reputation: 409
playing around with TS 3.7.2 I found a bizzarre edge case and now I'm curious of knowing more about how type inference in a function works in this specific case.
interface A {
functions: { t: number, a: { test: number }; };
}
interface B {
functions: { t: number, b: { ciao: number }; };
}
function combineStuff<TNamespace extends {}>(stuff: { functions: Partial<TNamespace> }[]): TNamespace {
// Implementation irrelevant for the question
return {functions: merge({}, ...stuff.map(s => s.functions))} as any;
}
const a: A = { functions: { t: 1, a: { test: 1 } } };
const b: B = {functions: { t: 1, b: { ciao: 2 } }};
const c = combineStuff([a, b]);
c.a.test;
c.b.ciao; // Error. Why is that?
const d = combineStuff<A['functions'] & B['functions']>([a, b]);
d.a.test;
d.b.ciao; // Works well
My expectation was that both c.b.ciao
and d.b.ciao
were type safe, but accessing c.b.ciao
causes an error.
From this simple example seems that the generic type could be automatically inferred from the elements of the array, this hypothesis is also supported by the fact that in the call combineStuff<A['functions'] & B['functions']>([a, b]);
the TS compiler verifies that the types of a
and b
are actually correct.
Additionally the type inference works correctly for the first element of the array a
, I can type-safely access to c.a.test
.
Why do I need to explicitly supply the generic type to combineStuff()
to obtain the correct result? Why is inference on the first element of the array working correctly?
Upvotes: 0
Views: 283
Reputation: 249466
The problem is that since you use Partial
in the array definition, TS will just pick one of the types (the first one it encounters) and then just check that the other type is assignable, which since we are talking about Partial
it will be.
The solution would be to use type parameter for the whole object. This will capture a union of A
and B
. We can then use an index type query (T['functions']
) to get a union of the function
types. And the use UnionToIntersection
shoutout jcalz to convert it to the desired intersection.
interface A {
functions: { t: number, a: { test: number }; };
}
interface B {
functions: { t: number, b: { ciao: number }; };
}
type UnionToIntersection<T> =
(T extends T ? (p: T) => void : never) extends (p: infer U) => void ? U : never;
function combineStuff<TNamespace extends Array<{ functions: object }>>(stuff: TNamespace): UnionToIntersection<TNamespace[number]['functions']>{
// Implementation irrelevant for the question
return null! as any;
}
const a: A = { functions: { t: 1, a: { test: 1 } } };
const b: B = { functions: { t: 1, b: { ciao: 2 } } };
const c = combineStuff([b, a]);
c.a.test;
c.b.ciao;
Upvotes: 2