Understanding TypeScript Conditional Types and infer
Conditional types enable runtime-like logic at the type level by letting you create types that change based on conditions. Combined with the infer keyword, they unlock pattern matching capabilities that extract types from complex structures, making it possible to build sophisticated type transformations that adapt to your data.
These features transform TypeScript's type system from static declarations into a computational engine that can inspect types, extract components, and make decisions based on type relationships. The result is reusable type utilities that work across different shapes while maintaining complete type safety.
In this guide, you'll learn:
- How conditional types work with the extends keyword
- Extracting types from complex structures using infer
- Building practical type utilities for real-world scenarios
Prerequisites
To follow this guide, you'll need Node.js 18+:
Setting up the project
Create and configure a new TypeScript project with ES module support:
Initialize with ES modules:
Install dependencies:
Next, create a TypeScript configuration file:
This initializes a tsconfig.json file, giving you a solid TypeScript setup. With these steps complete, you now have a modern TypeScript environment ready for exploring conditional types and the infer keyword with immediate code execution capabilities using tsx.
Understanding the type inflexibility problem
TypeScript's basic types work well for straightforward scenarios where you know exactly what types you need upfront. However, when building reusable utilities that must adapt to different input types, basic type aliases and interfaces fall short. You end up creating multiple versions of similar types or losing type information through overly broad generics.
This limitation becomes apparent when working with functions that return different types based on their input, API responses with varying shapes, or data transformations where output types depend on input structure. Basic generics can pass types through but can't make decisions or extract components based on type relationships.
Let's examine a function wrapper scenario where this inflexibility creates problems:
Run this code to see the type issues:
The code runs fine at runtime, but check what TypeScript thinks:
TypeScript rejects the function because response.data could be null when there's an error, but the function promises to return T. The union type creates ambiguity that basic generics can't resolve. You need conditional logic at the type level to express "if there's no error, return the data type; otherwise, throw."
This pattern scales poorly. Every function that needs type-level decisions requires workarounds like type assertions or overloads, cluttering your codebase with manual type management that should be automatic.
Solving the problem with conditional types
Conditional types introduce if-then logic at the type level using the syntax T extends U ? X : Y. When TypeScript evaluates this expression, it checks if type T is assignable to type U. If true, the conditional resolves to type X; otherwise, it resolves to type Y. This creates types that adapt based on type relationships.
The extends keyword in conditional types works like type compatibility checking—it asks "can T be used wherever U is expected?" This differs from extends in generic constraints, which restricts what types can be passed in. Conditional types make decisions after type parameters are known.
Let's fix the previous example with conditional types:
Check what happens when TypeScript validates this code:
TypeScript compiles successfully. The conditional type checks if the response has an error property, and if not, extracts the data type using infer. The function can now safely return the unwrapped data type because TypeScript understands the conditional logic at the type level.
Understanding conditional type mechanics
Conditional types work by evaluating type relationships during compilation. When TypeScript encounters T extends U ? X : Y, it performs structural compatibility checking between T and U. This happens after type parameters are resolved but before types are assigned to values.
The evaluation process:
- Type parameter resolution: TypeScript determines what
Tactually is from usage - Extends check: TypeScript checks if
T's structure is assignable toU - Branch selection: Based on the result, TypeScript selects either
XorYas the final type
The expression T extends { error: string } ? never : ... checks if T has an error property of type string. If true, the type becomes never (indicating this branch shouldn't be used). If false, TypeScript evaluates the next condition to extract the data type.
This differs from runtime conditionals—the checking happens entirely during compilation and produces zero runtime code. The conditional types guide TypeScript's type checking without affecting program execution.
Extracting types with the infer keyword
The infer keyword lets you extract type components from complex structures during conditional type evaluation. When you write T extends SomePattern<infer U>, TypeScript matches T against the pattern and captures the matching type component as U. This enables pattern matching at the type level.
The infer keyword only works within the extends clause of conditional types. TypeScript uses it to create a type variable that represents a piece of the matched type, making that extracted type available in the conditional's true branch.
Let's explore practical uses of infer with function types:
Run the code to see the extracted types in action:
The infer keyword captures the return type R from functions, the parameters tuple P, and even specific parameter types like the first parameter F. TypeScript validates that the extracted types match the actual function signatures, providing type safety without manual annotations.
Check that types are correctly inferred:
Check the validation:
TypeScript catches the type error because the extracted CreateUserReturn type knows that id must be a number. The infer keyword preserves complete type information from the original function signature.
Building practical type utilities
Conditional types with infer become powerful when building reusable utilities that work across different data structures. This pattern appears frequently in API clients, state management libraries, and data transformation pipelines where you need to derive types from existing structures automatically.
Let's create a comprehensive API response handler that demonstrates these capabilities:
Run the code to see the type utilities working:
The type utilities automatically extract the correct types from the API response structures. TypeScript knows that user has name and email properties, and that posts is an array with title and content properties—all without manual type annotations at the call sites.
Let's verify the type safety by adding invalid data:
Check the validation:
TypeScript catches the missing email property because the conditional types preserve complete structural information. The combination of conditional types and infer creates type utilities that adapt to different inputs while maintaining full type safety.
Advanced patterns with nested inference
Conditional types with multiple infer keywords can extract types from deeply nested structures. This enables sophisticated type transformations that traverse complex type hierarchies to pull out specific components.
Let's explore advanced inference patterns:
Run the code to see advanced type inference:
The recursive Awaited type unwraps nested Promises by calling itself with the inferred inner type until reaching a non-Promise type. The DeepValue type traverses nested object paths using template literal types and recursive inference to extract deeply nested property types.
These patterns demonstrate how conditional types with infer create composable type-level functions that can express complex transformations declaratively, making your types as expressive as your runtime code.
Final thoughts
Conditional types bring computational logic to TypeScript's type system through the extends ? : syntax, enabling types that adapt based on type relationships. The infer keyword adds pattern matching capabilities that extract type components from complex structures, making sophisticated type transformations possible.
The compile-time nature provides zero runtime overhead while dramatically improving type safety through automatic type derivation. This makes conditional types and infer valuable for any code that needs type-level decisions, whether for utility types, API clients, or data transformation pipelines.
These features transform TypeScript from a type annotation system into a type programming language where types can inspect, extract, and transform other types automatically, reducing manual type management while increasing safety.
Explore the TypeScript handbook on conditional types to learn more advanced patterns and discover how these features can enhance your application's type safety.