Concept
The beginner framing: infer lets a conditional type extract a piece of another type by pattern-matching against its structure, instead of just checking "does this match?", infer captures a specific part of what matched for use in the result.
type ElementType<T> = T extends (infer Item)[] ? Item : never;
type A = ElementType<string[]>; // string, extracted from the array shape
type B = ElementType<number>; // never, number doesn't match the array pattern at allinfer Item inside the extends clause introduces a new type variable, bound to whatever T actually matches at that position, here, an array's element type. This is the same mechanism ReturnType uses internally (extracting a function's return type by matching against a function-shaped pattern).
Position matters: infer combines candidates differently depending on where it appears
type ReturnOf<T> = T extends { a: () => infer R; b: () => infer R } ? R : never;
type Test = ReturnOf<{ a: () => string; b: () => number }>;
const x: Test = "hello"; // ✅
const y: Test = 42; // ✅, Test is `string | number`, a UNIONConfirmed by compiling this and checking assignability: when infer R appears more than once in covariant positions, return types, or generally "positions the type flows out of", TypeScript combines the multiple candidates into a union.
type ParamOf<T> = T extends { a: (x: infer P) => void; b: (x: infer P) => void } ? P : never;
type Test2 = ParamOf<{ a: (x: string) => void; b: (x: number) => void }>;
// Test2 resolves to `string & number`, which is effectively `never` for incompatible primitivesConfirmed by compiling this and verifying Test2 extends never holds true: when infer P appears in contravariant positions, function parameter types, "positions the type flows into", TypeScript instead combines the candidates into an intersection. For genuinely incompatible types like string and number, that intersection has no possible values, collapsing to something practically equivalent to never. This isn't an inconsistency between the two examples, it's the same variance-aware combination logic, applied consistently, just producing different-looking results because return positions and parameter positions have opposite variance.
Variance annotations: in and out
interface Producer<out T> { // T only ever comes OUT (covariant)
produce(): T;
}
interface Consumer<in T> { // T only ever goes IN (contravariant)
consume(value: T): void;
}
let dogProducer: Producer<string> = { produce: () => "dog" };
let broaderProducer: Producer<string | number> = dogProducer; // ✅ covariant: narrower → wider, allowed
let broadConsumer: Consumer
Confirmed by compiling both assignments: marking a type parameter out declares it's only ever used in output positions (like a return type), TypeScript already infers this automatically in most cases, but the explicit annotation documents the intent and lets the compiler give clearer error messages when variance is violated, and can measurably speed up type-checking for complex generic types by letting TypeScript skip re-deriving variance it's already been told. in is the mirror case for parameter/input-only positions. This directly connects to infer's union-vs-intersection combination behavior above, covariant (out) positions naturally combine as unions, contravariant (in) positions naturally combine as intersections, for the same underlying structural reason.