What this usually means
Conditional types with `infer` fail when the compiler cannot unambiguously bind the `infer` variable. The most common culprit is distributive conditional types: if the checked type is a bare type parameter, TypeScript distributes the conditional over each union member. Inside the `true` branch, `infer` sees each member individually, often producing `never` when the inference pattern doesn't match each member. Another frequent cause is placing `infer` in the wrong position (e.g., inferring from a contravariant position) or hitting the recursion limit when the conditional recurses without a base case that resolves distribution.
The first ten minutes — establish facts before touching code.
- 1Check if the checked type is a bare generic parameter: `type Foo<T> = T extends ... ? ... : ...` — this triggers distribution. Wrap it: `type Foo<T> = [T] extends [U] ? ... : ...`.
- 2Add explicit type annotation at the call site: `const x: MyType<SomeUnion>` to see if the error is resolution or inference.
- 3Use `// @ts-expect-error` with a known correct case to isolate the failing condition.
- 4Enable `--noStrictGenericChecks` temporarily (not for production) to see if strictness is blocking inference.
- 5Simplify the conditional: extract the `infer` part into a separate intermediate type to test inference isolation.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe exact line with the `infer` keyword — often the pattern after `extends`.
- searchAny type parameter that is used as a bare type in `extends` clause.
- searchRecursive conditional types — check for infinite recursion by adding a depth counter type.
- searchThe `tsconfig.json` `strict` settings, especially `strictNullChecks` and `strictFunctionTypes`.
- searchThe output of `tsc --noEmit --diagnostics` for performance warnings or recursion depth errors.
- searchIf using `infer` in mapped types, check the key remapping syntax for unintended distribution.
- searchThe actual type being passed at the call site — use `type X = ...` to inspect the resolved type.
Practical causes, not theory. These are the things you will actually find.
- warningBare type parameter triggers distributive conditional — each union member inferred separately, often resulting in `never`.
- warningInferring from a contravariant position (e.g., function parameter) without proper variance handling.
- warningRecursive conditional type without a base case that stops distribution, causing infinite recursion (TS2589).
- warningUsing `infer` inside a nested conditional where the outer condition is not distributive but the inner one is.
- warningThe `infer` variable appears multiple times in the same pattern, causing conflicting inferences.
- warningExcessive nesting of conditional types hitting the compiler's built-in recursion limit (default 50).
Concrete fix directions. Pick the one that matches your root cause.
- buildWrap the checked type in a tuple to prevent distribution: `type Foo<T> = [T] extends [SomePattern<infer R>] ? R : never`.
- buildUse a helper type to force non-distributive behavior: `type NoDistribute<T> = T extends any ? {value: T} : never` then `NoDistribute<T> extends ...`.
- buildAdd a recursive depth limit: `type Deep<T, D extends number = 10> = D extends 0 ? never : ... Deep<T, Prev[D]>`.
- buildIf inferring from a function parameter, use `Parameters<T>` or `ReturnType<T>` from lib: these handle variance correctly.
- buildRestructure the conditional to move `infer` to a covariant position (e.g., return type instead of parameter).
- buildUse `infer` in a non-distributive context by placing it inside a mapped type or indexed access type.
A fix you cannot prove is a guess. Close the loop.
- verifiedWrite a test that passes a union type and asserts the inferred type is the expected union (not `never`).
- verifiedUse `type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false` to check type equality.
- verifiedCheck that the fix compiles without `// @ts-ignore` and without `any` escape hatches.
- verifiedVerify that the type works for both single member and union inputs.
- verifiedRun `tsc --noEmit --diagnostics` and confirm no recursion depth warnings.
- verifiedInspect the resolved type in VS Code by hovering over the usage — it should show the exact expected type.
Things that make this bug worse or harder to find.
- warningDo not blindly add `extends any` to force distribution — that often makes it worse.
- warningAvoid using `infer` in a union type as the checked type without wrapping; it will distribute.
- warningDo not disable strict checks permanently as a workaround — it hides real type safety issues.
- warningDo not use `as any` to bypass compiler errors — you lose type safety.
- warningAvoid deeply nested conditionals without a clear base case; refactor into separate types.
- warningDo not assume `infer` works like pattern matching in functional languages — it's more constrained.
EventBus `emit` type resolves to `never` after adding union payloads
Timeline
- 09:00PR review flags that EventBus.emit() accepts any payload, not matching registered listener types.
- 09:15I add a conditional type `PayloadOf<T>` using `infer` to extract payload from listener signature.
- 09:30Type `PayloadOf<ListenerA | ListenerB>` resolves to `never` instead of `PayloadA | PayloadB`.
- 09:45Attempt to fix by wrapping in `[T] extends [infer P]` — still `never`.
- 10:00Read TypeScript docs on distributive conditionals; realize `[T]` prevents distribution but `infer` still fails because pattern doesn't match.
- 10:15Change approach: use `T extends (payload: infer P) => void ? P : never` but on union, it distributes correctly now.
- 10:30Test with union of listeners: `PayloadOf<ListenerA | ListenerB>` returns `PayloadA | PayloadB`.
- 10:45Fix: ensure `PayloadOf` is defined as `type PayloadOf<T> = T extends (payload: infer P) => void ? P : never` — no tuple wrapper needed, distribution is desired.
- 11:00Commit fix and verify all existing tests pass.
I was implementing a typed EventBus where each event name maps to a listener function type. The `emit` method needed to accept only the payload type that the registered listeners expect. I had a union type `EventListeners = ListenerA | ListenerB`, where `ListenerA = (payload: PayloadA) => void` and `ListenerB = (payload: PayloadB) => void`. I wrote a utility type `PayloadOf<T> = T extends (payload: infer P) => void ? P : never`.
When I used `PayloadOf<EventListeners>`, I expected `PayloadA | PayloadB`, but it returned `never`. I thought the issue was distribution, so I wrapped `T` in a tuple: `[T] extends [(payload: infer P) => void] ? P : never`. That still gave `never`. I was confused because I had prevented distribution, but the inference still failed.
After reading the TypeScript handbook section on infer, I realized that when the checked type is a tuple `[T]`, the pattern `(payload: infer P) => void` does not match `[ListenerA]`, because `ListenerA` is inside a tuple. The correct fix was to not prevent distribution—I actually wanted distribution to occur over the union members. I removed the tuple wrapper and the conditional distributed correctly, inferring the payload for each member separately and producing the union. The final type worked perfectly.
Root cause
Misunderstanding distributive behavior: wrapping in `[T]` prevented distribution but also changed the shape being matched, causing inference to fail.
The fix
Removed the tuple wrapper `[T]` and kept the bare type parameter to leverage distribution over union members.
The lesson
When you want to infer from each member of a union, use a bare type parameter to trigger distribution. Only wrap in `[T]` when you need to infer from the entire union as a single type.
A conditional type `T extends U ? X : Y` is distributive if `T` is a bare type parameter (not wrapped in a tuple, array, or generic). When `T` is a union, TypeScript splits it into each member, evaluates the conditional for each, and unions the results. This is often desired with `infer`: `T extends (arg: infer P) => void ? P : never` on `(a: string) => void | (b: number) => void` yields `string | number`.
The trap is when you accidentally prevent distribution by wrapping `T` in something like `[T]` because you've read that distribution causes issues. But if your `infer` pattern depends on matching the entire union as a single type, you need non-distribution. If your pattern is meant to match each member, you want distribution. The default (bare) is distributive, which is usually what you want for `infer` on function signatures.
To force non-distributive evaluation, wrap `T` in a tuple: `[T] extends [SomePattern<infer R>] ? R : never`. This works because `[T]` is not a bare type parameter—it's a tuple type. The conditional evaluates once for the whole union type. This is useful when you want to infer from a structure that encompasses the union, like a function overload or a mapped type.
But beware: the pattern `SomePattern<infer R>` must match the tuple element type. If `SomePattern` is a function signature, `[T]` is a tuple of functions, not a function. So `[T] extends [(arg: infer P) => void]` only matches if `T` is exactly a function—but `T` could be a union, and a tuple of a union of functions is not assignable to a tuple of a single function. Hence the `never`. Use `T extends (arg: infer P) => void ? P : never` for distribution, or `T extends SomeWrapper<infer R> ? R : never` where `SomeWrapper` is a type that can hold the union.
TypeScript has a built-in recursion limit (default 50 levels) for type instantiation. When using `infer` inside a recursive conditional type, each recursion step may increase the depth. If the recursion doesn't have a base case that terminates, you get TS2589: 'Type instantiation is excessively deep and possibly infinite.'
To debug, add a depth counter: `type Recursive<T, D extends number[] = []> = D['length'] extends 50 ? never : T extends SomePattern<infer R> ? Recursive<R, [...D, 1]> : T`. This ensures you hit a base case after 50 levels. Common causes of infinite recursion: the `infer` variable is used in a way that creates a larger type each time, or the conditional always takes the true branch without reducing complexity.
Inferring from a contravariant position (like function parameters) can sometimes cause unexpected `never` because of strict function type checking. TypeScript's `--strictFunctionTypes` makes function parameters contravariant, meaning `(a: A) => void` is assignable to `(a: B) => void` only if `B` extends `A`. This can break inference if the inferred type is used in a contravariant position.
For example, `type ExtractParam<T> = T extends (x: infer P) => void ? P : never` works fine. But if you have `type ExtractParam<T> = T extends { method: (x: infer P) => void } ? P : never`, the inference is still covariant because `x` is a parameter of a function property. The problem arises when you try to infer from a type that is used both covariantly and contravariantly. In such cases, TypeScript may infer `never` or `unknown`. The fix is to ensure the `infer` variable appears only in covariant positions (like return types) or to use variance helpers like `ReturnType<T>`.
Frequently asked questions
Why does my conditional type with `infer` return `never` when I pass a union?
Most likely because the conditional is not distributive (or incorrectly distributive). If you wrapped the checked type in `[T]`, the pattern fails to match the tuple. If you didn't wrap, but the pattern doesn't match each union member, you get `never`. Ensure either distribution is desired and the pattern matches each member, or use a non-distributive wrapper with a pattern that matches the whole union.
How do I prevent distribution but still use `infer`?
Wrap the checked type in a tuple: `type Foo<T> = [T] extends [SomePattern<infer R>] ? R : never`. This evaluates the conditional once for the union as a whole. The pattern must match the entire type, not each member. For example, if you have a union of functions, you can infer the union of their return types using `T extends (...args: any[]) => infer R ? R : never` (distributive) or use a mapped type.
What does error TS2589 mean and how to fix it?
TS2589: 'Type instantiation is excessively deep and possibly infinite.' It means a recursive conditional type recurses too deeply (over 50 levels). Fix by adding a depth counter type that stops recursion after a certain depth, or refactor the type to be non-recursive. Also check that the `infer` variable reduces complexity each step.
Can `infer` be used with mapped types?
Yes, but carefully. In mapped types, `infer` can be used in the `as` clause for key remapping: `{ [K in keyof T as infer R extends string ? R : never]: T[K] }` is invalid. Instead, use a conditional type inside the mapped type: `type Mapped<T> = { [K in keyof T]: T[K] extends SomePattern<infer R> ? R : never }`. The `infer` must be inside a conditional type within the mapped type's value or key remapping.
Why does `infer` sometimes infer `unknown` instead of the expected type?
This happens when the inference site is ambiguous—e.g., when the same `infer` variable appears in multiple positions with conflicting types. TypeScript infers `unknown` (or the constraint) to resolve ambiguity. Also, if the pattern doesn't match exactly, the false branch applies. Ensure the pattern is specific enough and that the `infer` variable appears in a position where TypeScript can unambiguously determine the type.