Skip to content

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

FixDevs ·

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.

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 and Fix: TypeScript Template Literal Type 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