In TypeScript, when the same `infer U` name appears at two positions of one conditional type's `extends` clause, when do you get a union of the candidates and when do you get an intersection?
answer
- two candidates, one name
- the position decides how they combine
- parameters are the contravariant case
- properties union, parameters intersect
- an uninhabitable parameter type is the tell
basics
~10 sMultiple candidates for one inference variable are combined by position: covariant positions such as property or return types produce a union of the candidates, while contravariant positions such as function parameters produce an intersection.
solid answer
~50 sReusing an `infer` name gives the compiler several candidates for one variable, and it combines them according to the variance of the positions they came from. In covariant positions — property types, array elements, return types — the candidates are unioned: `T extends { a: infer U; b: infer U } ? U : never` applied to `{ a: string; b: number }` yields `string | number`. In contravariant positions — function *parameter* types — they are intersected instead: the same trick over two callbacks taking `string` and `number` yields `string & number`, which nothing can satisfy. That is not a bug; it is what soundness requires, because a value must be acceptable to *both* callbacks. In practice this bites when someone reuses a name expecting "either one" and the resulting parameter type turns out to be uninhabitable. The fix is almost always to use two distinct `infer` names and combine them explicitly.
code
typescript · 18 linestype Co<T> = T extends { a: infer U; b: infer U } ? U : never;
type X = Co<{ a: string; b: number }>; // string | number
type Contra<T> = T extends {
f: (x: infer U) => void;
g: (x: infer U) => void;
} ? U : never;
type Y = Contra<{ f: (x: string) => void; g: (x: number) => void }>;
// string & number: no value satisfies both
type Fixed<T> = T extends {
f: (x: infer A) => void;
g: (x: infer B) => void;
} ? A | B : never;
type Z = Fixed<{ f: (x: string) => void; g: (x: number) => void }>; // string | number
const ok: X = 42;
console.log(ok);go deeper
Know that an inference name can appear more than once in one pattern, and that if you want two separate pieces of a type you should give them two separate names.
Explain that multiple candidates for one name get combined, and demonstrate the property-position case producing a union of the candidate types.
Diagnose the contravariant case in real code: recognise an uninhabitable parameter type as a reused inference name, and rewrite the helper with distinct names and an explicit combination.
Decide whether helpers that depend on variance rules belong in shared code at all, since their failure mode is an error message that only the author can decode.
## Two candidates, one variable A single `extends` clause may mention the same inference name more than once: ```ts type Both<T> = T extends { a: infer U; b: infer U } ? U : never; ``` When the compiler matches `{ a: string; b: number }` against that pattern, it collects *two* candidates for `U`. It then has to produce one type, and the rule it uses depends on where the candidates were found. ## The rule **Covariant positions produce a union.** A covariant position is one where a more specific type is safely usable in place of a wider one: object property types, array element types, function return types. Combining candidates there with a union is safe, because a value in that position is one or the other. ```ts type Co<T> = T extends { a: infer U; b: infer U } ? U : never; type X = Co<{ a: string; b: number }>; // string | number ``` **Contravariant positions produce an intersection.** A parameter type is contravariant: a function that accepts a *wider* type can stand in for one that accepts a narrower type. If one candidate came from a parameter accepting `string` and another from a parameter accepting `number`, the only type usable in *both* places is one that is simultaneously both. ```ts type Contra<T> = T extends { f: (x: infer U) => void; g: (x: infer U) => void; } ? U : never; type Y = Contra<{ f: (x: string) => void; g: (x: number) => void }>; // string & number — nothing satisfies it ``` ## Why the diagnosis matters The symptom in a real codebase is a helper that "works" until someone passes two handlers with different payloads, and then every call site fails with an error mentioning a type nothing can produce. Engineers who do not know this rule tend to blame the call site and start adding assertions, which buries the problem instead of fixing it. Recognising an intersection of unrelated primitives as *the signature of a reused inference name in parameter position* is the whole skill here. The same rule is what powers the well-known union-to-intersection trick, where a helper deliberately places `infer U` in a parameter position precisely to obtain an intersection. So the behaviour is exploited as often as it is stumbled over. ## Fixing it Use distinct names and combine them yourself — explicitly, and visibly at the point of the decision: ```ts type Payload<T> = T extends { f: (x: infer A) => void; g: (x: infer B) => void; } ? A | B : never; ``` Now the union is your choice rather than an emergent property of variance, and a future reader can see it. If you genuinely want only one of the positions, infer only that one and leave the other slot as `any` or `never` in the pattern. A second option is to restructure the input type so the shared piece is named once — for example a single generic parameter on the source interface, which the pattern can then capture in one place. That is usually the better fix when you own the type being matched: it removes the ambiguity at the source instead of resolving it at the extraction site. ## Where the boundary of this rule is This is purely about how *one* conditional combines *multiple candidates for one name*. It is not about how a conditional behaves when the type being checked is itself a union — that is a different mechanism with different rules. Keeping the two apart is worth doing deliberately in an interview, because they produce superficially similar unions from very different causes. And, as always in this layer, nothing here exists at runtime: the compiler resolves the candidates during checking and emits no trace of the reasoning.
- Why is intersecting the candidates the sound choice for parameter positions rather than a design accident?Because a parameter position is contravariant: whatever type you pick has to be accepted by *every* function that slot stands for. A union would allow a value acceptable to only one of them, which would let unsound calls through. The intersection is the only type guaranteed usable at all the matched positions.
- A colleague's helper returns a parameter type that no value satisfies. What do you check first?Look for one `infer` name reused across two or more parameter positions in the same extends clause. That is the standard cause. Confirm it by giving each position its own name and printing the two candidates separately; then decide explicitly whether the helper should union them or pick one.
- How would you rewrite such a helper so the combining rule is visible to the next reader?Give each position a distinct inference name and write the combination yourself in the true branch — `A | B` if either is acceptable, `A & B` only if you really mean both. The behaviour is then a stated decision rather than an emergent consequence of variance that a reader has to reconstruct.
saying these in an interview costs you the question
- Assumes the compiler simply takes the first candidate
- Thinks reusing an infer name is a compile error
- Says parameter positions union just like property positions
- Expects a value to exist for an intersection of unrelated primitives
- Blames the call site and adds an assertion instead of fixing the helper