Fix: TypeScript Conditional Types Not Working — infer Not Extracting, Distributive Behavior Unexpected, or Type Resolves to never
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 behaviorOr 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 : YwithT = A | Bbecomes(A extends U ? X : Y) | (B extends U ? X : Y). inferonly works in theextendsclause —infercan only appear on the right side ofextendsin 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.,
Tis still a generic), the conditional type stays unresolved. You may seeT extends Foo ? X : Yin hover types instead ofXorY. neveras input causesneveroutput —never extends anythingisnever, not the true branch. Distributing overneveralways producesnever.
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>; // falseFix 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>>>; // stringExtract 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>; // stringFix 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>; // falseStill 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: TypeScript Discriminated Union Error — Property Does Not Exist or Narrowing Not Working
How to fix TypeScript discriminated union errors — type guards, exhaustive checks, narrowing with in operator, never type, and common patterns for tagged unions.
Fix: TypeScript Template Literal Type Error — Type Not Assignable or Inference Fails
How to fix TypeScript template literal type errors — string combination types, conditional inference, Extract and mapped types with template literals, and common pitfalls.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.