Gianluca Venturini
Gianluca Venturini

Reputation: 409

TypeScript type inference from array passed as function parameter

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

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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; 

Playground Link

Upvotes: 2

Related Questions