Skip to content

Fix: TypeScript Conditional Types Not Working — infer Not Extracting, Distributive Behavior Unexpected, or Type Resolves to never

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix TypeScript conditional type issues — infer keyword usage, distributive conditional types, deferred evaluation, naked type parameters, and common conditional type patterns.

The Problem

A conditional type using infer resolves to never instead of the expected type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Result = ReturnType<string>;  // never — why?
// Expected: should be 'never' here since string isn't a function, but...

type Fn = (x: number) => string;
type R = ReturnType<Fn>;  // string — this works
// But when applied to a union: what happens?

Or distributive behavior produces unexpected union types:

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// Expected: (string | number)[]
// Got: string[] | number[]   ← distributive behavior

Or infer doesn’t extract the type you expect from a generic:

type ElementType<T> = T extends Array<infer E> ? E : never;

type R1 = ElementType<string[]>;   // string — works
type R2 = ElementType<string[][]>; // string[] — gets outer element, not inner
type R3 = ElementType<[string, number]>; // string | number — is this right?

Why This Happens

TypeScript’s conditional types have several non-obvious behaviors:

  • Distributive conditional types — when the checked type is a “naked” (unwrapped) type parameter, conditional types distribute over unions automatically. T extends U ? X : Y with T = A | B becomes (A extends U ? X : Y) | (B extends U ? X : Y).
  • infer only works in the extends clauseinfer can only appear on the right side of extends in a conditional type. It captures the type that matches that position in the structure.
  • Deferred evaluation — when TypeScript can’t determine which branch to take (e.g., T is still a generic), the conditional type stays unresolved. You may see T extends Foo ? X : Y in hover types instead of X or Y.
  • never as input causes never outputnever extends anything is never, not the true branch. Distributing over never always produces never.

The deeper reason conditional types feel hostile is that they operate in a different world from the runtime code they describe. Conditional types are evaluated by the TypeScript compiler at type-check time using a small set of operations: structural assignability, distributivity over unions, infer capture, and recursion bounded by an internal depth limit. Most “this should work” surprises come from treating conditional types as runtime branches and forgetting that the operands are still types — T extends string is not running typeof T === 'string', it is asking whether every value of type T is also a value of type string. Generic T is therefore neither — it is “still unknown,” and the conditional cannot decide yet, so it stays as a deferred type expression.

The second source of confusion is that conditional types over a “naked” generic parameter distribute automatically over unions, which is occasionally what you want and frequently catastrophic. T extends string ? T[] : never with T = string | number does not return (string | number)[] — it distributes into (string extends string ? string[] : never) | (number extends string ? number[] : never), simplifying to string[]. If you wanted the union as a single array type, you must wrap both sides in a tuple to suppress distribution: [T] extends [string] ? T[] : never. Once you internalize this rule, half of the conditional-type bugs in production codebases become obvious.

How Other Tools Handle This

Conditional types are TypeScript’s mechanism for expressing type-level computation; equivalent or analogous features exist in other typed languages with very different trade-offs.

TypeScript conditional types vs Flow type-level branching. Flow’s type system supports type-level conditionals through $Call, $ObjMap, $TupleMap, and $Shape, but does not have an extends ... ? : : ternary syntax. Type-level logic in Flow is typically written as a chain of utility types, each of which performs a single transformation. The result is more verbose but arguably more readable; the cost is that Flow cannot express recursive type-level computations as compactly as TypeScript can with T extends infer X ? ... self-referential conditionals. For library authors maintaining both Flow and TypeScript type definitions, conditional types are usually the hardest piece to port — many advanced TypeScript utility types simply have no Flow equivalent.

TypeScript vs Rust trait bounds. Rust’s where T: Display + Send clauses serve a similar purpose to T extends Display & Send, but they are evaluated by the trait system rather than as structural assignability. Rust does not have infer because traits expose associated types explicitly: T: Iterator<Item = u32> extracts the iterator’s item type without an infer keyword. Rust’s impl Trait and GATs (generic associated types, stabilized in 2022) approach the same problems as infer in conditional types but require more upfront declaration. The trade-off is that Rust’s type-level computation is more disciplined and easier to reason about; TypeScript’s is more flexible but can produce types so complex that the IDE hover shows pages of resolution.

TypeScript vs Haskell type families. Haskell’s type families and closed type families are the closest analog to TypeScript conditional types. A closed type family type family ReturnType (a :: Type) :: Type where ReturnType (a -> b) = b; ReturnType a = a performs the same pattern matching as T extends (...args: any[]) => infer R ? R : T. Haskell’s GHC handles termination and confluence checking at compile time, refusing to compile recursive type families that might not terminate; TypeScript imposes a depth limit (~50 in current versions) but does not statically prove termination. Haskell’s type system is also fully decidable in the relevant fragment, while TypeScript’s is intentionally not (Turing-complete in conditional types since at least 4.1).

TypeScript vs Kotlin reified type parameters. Kotlin’s inline fun <reified T> typeOf(): KClass<T> provides runtime access to generic type information via reification — at compile time, the compiler substitutes the concrete type at every call site. TypeScript has no equivalent because TypeScript types are fully erased at runtime; conditional types live entirely at the type level. For code that must branch on type information at runtime, Kotlin’s reification or Java’s Class<T> parameters are the only options; TypeScript developers achieve similar effects with type guards (x is Foo) plus a runtime tag (x.kind === 'foo').

Type-level vs value-level computation. TypeScript’s conditional types let you compute one type from another but cannot directly drive runtime behavior — there is no if typeof T extends string at runtime. The bridge is usually a function overload set (compile-time branching on argument type) or a discriminated union with a tag field (runtime branching that the type checker can follow). Languages with dependent types (Idris, Agda, Lean) collapse this distinction by allowing values to appear in types; TypeScript intentionally stops short of that to keep type-checking fast and decisions explainable.

In Production: Incident Lens

The most common conditional-type incident in production is a utility type that suddenly resolves to never after a refactor and silently disables type checking downstream. You wrote type ExtractData<T> = T extends { data: infer D } ? D : never; last year, refactored the API response to wrap data inside result.data.payload, and now ExtractData<ApiResponse> is never. Every function that takes the extracted type accepts anything (because never is assignable to every type), and the type checker stops protecting you. Add // @ts-expect-error or Equals<X, never> test assertions in your type-test suite to catch this — the expect-type library and tsd both surface “this resolved to never” failures explicitly.

The second pattern is distributive surprise inside Pick, Omit, or custom mappings. You wrote type WithoutNulls<T> = { [K in keyof T]: T[K] extends null ? never : T[K] }; expecting to strip null-typed properties, but because T[K] extends null distributes when T[K] = string | null, the result for that property becomes string | never (i.e. string) instead of the property being removed. To remove the property entirely, you need a two-step pattern: collect keys whose value is non-null with KeysOfType, then Pick over that set. Always test distributive behavior on union inputs before shipping a conditional utility.

The third recurring failure is hitting the recursion depth limit during build. A deep DeepPartial-style utility type works fine on small types in tests but TypeScript bails with “Type instantiation is excessively deep and possibly infinite” on real API response types that have 10+ nested levels. The fix is to introduce a manual depth counter as a type parameter or to limit recursion at known fan-out points (arrays of object arrays). Profile type-check time with tsc --extendedDiagnostics and check for Types: 800000+ numbers — they indicate a conditional type expanding combinatorially.

Fix 1: Control Distributive Behavior with Tuple Wrapping

Wrap the type in a tuple to prevent distribution over unions:

// DISTRIBUTIVE — naked type parameter distributes over unions
type ToArray<T> = T extends any ? T[] : never;
type D = ToArray<string | number>;
// Result: string[] | number[]   (distributes)

// NON-DISTRIBUTIVE — tuple wrapping prevents distribution
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type ND = ToArrayND<string | number>;
// Result: (string | number)[]   (no distribution)

When you want distributive behavior:

// Distributive is useful for filtering unions
type NonNullable<T> = T extends null | undefined ? never : T;
// string | null | undefined → string  (filters out null and undefined)

type OnlyStrings<T> = T extends string ? T : never;
type R = OnlyStrings<string | number | boolean>;
// Result: string  (filters to strings only)

When you don’t want distributive behavior:

// Check if a type IS a union (not distributive)
type IsUnion<T, U = T> = [T] extends [U] ? false : true;
// IsUnion<string | number> → true
// IsUnion<string> → false

// Non-distributive equality check
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
type E1 = Equals<string, string>;       // true
type E2 = Equals<string | number, string | number>;  // true
type E3 = Equals<string, string | number>;  // false

Fix 2: Use infer to Extract Nested Types

infer captures a type at a specific position in the extends clause:

// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = ReturnType<() => string>;     // string
type R2 = ReturnType<(n: number) => boolean>;  // boolean

// Extract function parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type P1 = Parameters<(a: string, b: number) => void>;  // [string, number]

// Extract first parameter only
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>;  // string

// Extract Promise value
type Awaited<T> = T extends Promise<infer V> ? V : T;
type A1 = Awaited<Promise<string>>;  // string
type A2 = Awaited<string>;           // string (not wrapped)

// Recursive Awaited (handles nested promises)
type DeepAwaited<T> = T extends Promise<infer V> ? DeepAwaited<V> : T;
type DA = DeepAwaited<Promise<Promise<string>>>;  // string

Extract from more complex structures:

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>;     // string
type E2 = ElementOf<number[][]>;   // number[]  (one level)

// Extract object value types
type ValueOf<T> = T extends Record<string, infer V> ? V : never;
type V1 = ValueOf<{ a: string; b: number }>;  // string | number

// Extract constructor parameter types
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
type CP = ConstructorParams<typeof Date>;  // [value?: string | number | Date]

// Extract from mapped types
type UnwrapPromises<T> = {
  [K in keyof T]: T[K] extends Promise<infer V> ? V : T[K];
};
type Unwrapped = UnwrapPromises<{
  name: string;
  data: Promise<number[]>;
}>;
// { name: string; data: number[] }

Fix 3: Understand Deferred Resolution

Conditional types on generic type parameters are deferred until the type is instantiated:

// This stays unresolved inside generic functions
function wrap<T>(value: T): T extends string ? string[] : number[] {
  // TypeScript can't resolve the conditional yet — T is still generic
  if (typeof value === 'string') {
    return [value] as any;  // Must use 'as any' — TypeScript can't narrow conditionals
  }
  return [0] as any;
}

// At call site, it resolves correctly:
const r1 = wrap('hello');   // string[]
const r2 = wrap(42);        // number[]

Work around deferred resolution with overloads:

// Function overloads let TypeScript choose the right signature
function process(value: string): string[];
function process(value: number): number[];
function process(value: string | number): string[] | number[] {
  if (typeof value === 'string') return [value];
  return [value];
}

const s = process('hi');   // string[]
const n = process(42);     // number[]

Use Extract and Exclude for simpler filtering:

// Instead of complex conditional types, use built-in utilities
type StringsOnly = Extract<string | number | boolean, string>;  // string
type NoStrings = Exclude<string | number | boolean, string>;    // number | boolean
type NoNulls = NonNullable<string | null | undefined>;          // string

Fix 4: Build Practical Utility Types with Conditionals

Real-world patterns that use conditional types effectively:

// Deep readonly
type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonly<E>>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type Config = DeepReadonly<{
  server: { host: string; port: number };
  features: string[];
}>;
// All nested properties become readonly

// Deep partial
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// Pick keys by value type
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

type User = { id: number; name: string; email: string; age: number };
type StringKeys = KeysOfType<User, string>;  // "name" | "email"
type NumberKeys = KeysOfType<User, number>;  // "id" | "age"

// Make specific keys required
type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;

type UserForm = WithRequired<Partial<User>, 'name' | 'email'>;
// { id?: number; name: string; email: string; age?: number }

Flatten union of function signatures:

// Get all possible return types
type EventHandlers = {
  onClick: (e: MouseEvent) => void;
  onKeyDown: (e: KeyboardEvent) => boolean;
  onChange: (value: string) => Promise<void>;
};

type HandlerReturnTypes = ReturnType<EventHandlers[keyof EventHandlers]>;
// void | boolean | Promise<void>

Fix 5: Conditional Types with Template Literals

Combine conditional types with template literal types for string manipulation:

// Extract route params from a path string
type RouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof RouteParams<`/${Rest}`>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type Params = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }

// CamelCase to snake_case
type CamelToSnake<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase<Head>
      ? `_${Lowercase<Head>}${CamelToSnake<Tail>}`
      : `${Head}${CamelToSnake<Tail>}`
    : S;

type Snake = CamelToSnake<'camelCaseString'>;  // 'camel_case_string'

// Event name type checking
type EventName<T extends string> = T extends `on${Capitalize<string>}` ? T : never;
type ValidEvent = EventName<'onClick' | 'onHover' | 'click' | 'hover'>;
// 'onClick' | 'onHover'

Fix 6: Debug Conditional Types

When a conditional type produces an unexpected result, use these debugging techniques:

// Technique 1: Simplify to isolate the issue
type Debug<T> = T extends string ? 'is string' : 'not string';
type D1 = Debug<string>;        // 'is string'
type D2 = Debug<number>;        // 'not string'
type D3 = Debug<string | number>;  // 'is string' | 'not string' — distributive!

// Technique 2: Use a type alias to inspect intermediate types
type InferTest<T> = T extends Array<infer E> ? E : 'not array';
type IT1 = InferTest<string[]>;      // string
type IT2 = InferTest<number[][]>;    // number[]  (outer array's element)
type IT3 = InferTest<[1, 2, 3]>;     // 1 | 2 | 3  (tuple element union)

// Technique 3: Check what 'never' extends
type NeverTest<T> = [T] extends [never] ? 'is never' : 'not never';
type NT1 = NeverTest<never>;   // 'is never'
type NT2 = NeverTest<string>;  // 'not never'
// Note: 'never extends X' with naked T distributes to never, not true/false
// Use [T] extends [never] for reliable never checking

// Technique 4: Hover over types in IDE to see resolved values
type Resolved = ReturnType<typeof fetch>;
// Hover shows: Promise<Response>

Common never pitfalls:

// Conditional type with 'never' input always returns 'never'
type Test<T> = T extends string ? 'yes' : 'no';
type R = Test<never>;  // never  (not 'no'!)

// Safe never check
type IsNever<T> = [T] extends [never] ? true : false;
type I1 = IsNever<never>;   // true
type I2 = IsNever<string>;  // false

Still Not Working?

infer in a covariant position vs contravariant position — when infer appears in multiple places in a union, TypeScript infers the intersection (contravariant position) or union (covariant position). For function parameters (contravariant), inferring the same variable at multiple positions produces an intersection:

type IntersectParams<T> =
  T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;

type I = IntersectParams<{ a: (x: string) => void; b: (x: number) => void }>;
// Result: string & number = never  (intersection of contravariant positions)

Conditional types don’t narrow inside function bodies — even if you’ve narrowed a generic T with a conditional type in the return type, TypeScript doesn’t use that information inside the function body. Use type assertions (as) for the implementation and rely on the signature for caller safety.

Circular conditional types cause Type alias circularly references itself errors — recursive conditional types are supported but must have a base case and must be “productive” (each recursive call handles a smaller type). Adding & {} or using intermediate type aliases can sometimes help TypeScript recognize termination.

For related TypeScript issues, see Fix: TypeScript Mapped Type Error, Fix: TypeScript Template Literal Type Error, Fix: TypeScript Discriminated Union Error, and Fix: TypeScript Generic Constraint Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles