Using infer
with Objects in Conditional Types — Practical Guide
Introduction
TypeScript's type system is powerful, and one of its most expressive features is conditional types paired with the infer keyword. For intermediate developers building libraries, frameworks, or complex application types, knowing how to extract, transform, and infer properties from object types can save time and eliminate classes of runtime errors. In this tutorial you'll learn how infer works specifically with object (and object-like) shapes in conditional types, enabling you to build utility types that read and reshape types automatically.
We'll cover basic inference patterns, common real-world scenarios (like extracting event payloads, props, and return types), and how to combine infer with mapped types, unions, and intersections for highly composable types. You'll find step-by-step examples, code snippets, troubleshooting tips, and advanced techniques for optimizing your types for readability and performance. By the end of this article you'll be able to author robust TypeScript utilities such as ExtractProps, PickByValue, FlattenObjects, and type-safe function overload helpers — all powered by infer and conditional types.
This guide also links to related topics like mapped types and discriminated unions, and it assumes you already know the basics of TypeScript types. If you need a refresher on class basics or when to use interfaces vs type aliases, the article includes pointers to those topics as you go.
Background & Context
Conditional types let you compute a new type based on whether a type extends another. The infer keyword lets you declare a type variable inside the conditional arm and capture a part of the matched type. With object types, infer becomes especially valuable for extracting property sets or nested shapes without manually repeating type definitions.
Using infer effectively reduces duplication and improves maintainability. It fits naturally with mapped types and unions for building generic utilities. For deeper reading on mapped type modifiers (useful when transforming inferred properties), see Advanced Mapped Types: Modifiers (+/- readonly, ?). When your inferred types interact with classes and implementations, the class guides linked below will help ground object inference in practical codebases.
Key Takeaways
- How infer works inside conditional types to capture parts of object types.
- Patterns for extracting property names, values, and nested shapes with infer.
- Combining infer with mapped types, union, and intersection types for robust utilities.
- Practical utilities: ExtractProps, PickByValue, Flatten, and event payload extraction.
- Performance and readability tips to avoid overly complex inferred types.
Prerequisites & Setup
To follow the examples you'll need Node.js and a TypeScript project (TS 4.1+ recommended, though some examples use features available in TS 4.4+). Install TypeScript with npm if needed and use an editor that understands TypeScript (VS Code recommended). A local tsconfig with strict enabled helps catch mistakes early.
Basic knowledge assumed:
- Type aliases and interfaces (see our comparison guide: Differentiating Between Interfaces and Type Aliases in TypeScript).
- Mapped types (review: Advanced Mapped Types: Modifiers (+/- readonly, ?)).
- Class basics if you want to infer constructor types (see: Introduction to Classes in TypeScript: Properties and Methods).
Create a starter project:
- npm init -y
- npm i -D typescript
- npx tsc --init
- Set "strict": true in tsconfig.json
Now you are ready to experiment with the code snippets below.
Main Tutorial Sections
1) Basic infer with Tuples vs Objects
infer is often demonstrated with tuples (e.g., extracting the first element). With objects the pattern is similar but you match properties. Example: extract the type of a property named payload when present.
type HasPayload<T> = T extends { payload: infer P } ? P : never; type E1 = HasPayload<{ payload: string }>; // string type E2 = HasPayload<{ data: number }>; // never
This simple conditional captures the type P when T contains payload. Notice infer only works when the conditional branch matches the structure; otherwise it falls to the fallback.
2) Extracting All Values of an Object
You can infer the union of all property value types using an index signature pattern. This is helpful to compute value unions.
type Values<T> = T extends { [K in keyof T]: infer V } ? V : never; type Example = { a: string; b: number }; type V = Values<Example>; // string | number
The pattern expands keys then infers a single V that becomes the union of the property types.
3) Extracting Keys by Value Type (PickByValue)
infer can be combined with key mapping to pick keys with a specific value type.
type KeysOfType<T, Match> = { [K in keyof T]: T[K] extends Match ? K : never }[keyof T]; type PickByValue<T, Match> = Pick<T, KeysOfType<T, Match>>; type Foo = { a: string; b: number; c: string }; type StrKeys = KeysOfType<Foo, string>; // "a" | "c" type OnlyStrings = PickByValue<Foo, string>; // { a: string; c: string }
While this example doesn't use infer directly, it demonstrates how extracting keys and values plays with conditional mapping. You can add infer if you want to capture complex value shapes.
See also patterns with mapped type modifiers if you need to toggle readonly or optional modifiers after inference: Advanced Mapped Types: Modifiers (+/- readonly, ?).
4) Inferring Nested Shapes
To infer nested properties, pattern-match the nested structure with infer inside the object pattern. This is useful for event payloads or nested API responses.
type ExtractNested<T> = T extends { meta: { result: infer R } } ? R : never; type ApiResponse = { meta: { result: { id: string } }; status: 200 }; type Res = ExtractNested<ApiResponse>; // { id: string }
This pattern is safe only when the nested path reliably exists; otherwise combine with unions or optional chaining in types.
5) Discriminated Unions and infer
When combined with discriminated unions, infer helps extract the payload of a particular case. This is common in action/event systems.
type Action = | { type: 'ADD'; payload: { id: number } } | { type: 'DEL'; payload: { id: string } } | { type: 'RESET' }; type PayloadOf<T, U extends T['type']> = T extends { type: U; payload: infer P } ? P : never; type AddPayload = PayloadOf<Action, 'ADD'>; // { id: number }
Using infer here yields typed payloads per action case, reducing switch boilerplate and enabling typed reducers or handlers.
If you need exact value types and discriminants, review literal types and const assertions: Literal Types: Exact Values as Types.
6) Inferring from Function-like Objects (Handlers)
If you store handlers as objects or maps, you can infer param and return types for each handler to build typed wrappers.
type HandlerMap = { add: (x: number) => number; log: (msg: string) => void; }; type HandlerParams<H> = H extends (...args: infer A) => any ? A : never; type AddParams = HandlerParams<HandlerMap['add']>; // [number]
Now you can build a wrapper to call handlers with proper types, e.g., a dispatch method that infers params from the key and handler map.
7) Inferring Constructor/Instance Types from Classes
Infer is useful when you want to extract instance or parameter types from constructors. When classes are involved you may mix with class-based patterns — see the intro to classes for background: Introduction to Classes in TypeScript: Properties and Methods.
type ConstructorParams<T> = T extends new (...args: infer A) => any ? A : never; class Service { constructor(public url: string, private timeout: number) {} } type SParams = ConstructorParams<typeof Service>; // [string, number]
This approach is great for factory helpers and dependency injection containers that must reflect constructor signatures.
For patterns where you implement interfaces with classes or use abstract classes, see: Implementing Interfaces with Classes and Abstract Classes: Defining Base Classes with Abstract Members.
8) Mapping Over Objects and Using infer with Mapped Types
Often you want to map over an object's properties but use infer to compute new value types. Combine mapped types with conditional + infer to create advanced transformations.
type UnwrapPromises<T> = { [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }; type Promised = { a: Promise<string>; b: number }; type Unwrapped = UnwrapPromises<Promised>; // { a: string; b: number }
This pattern converts Promise-wrapped values to their resolved types across an object shape — useful in utilities that fetch many async fields.
For more on mapped modifiers (optional/readonly) when transforming object shapes, check Advanced Mapped Types: Modifiers (+/- readonly, ?).
9) Infer with Intersection and Union Types
When conditional types distribute over unions, infer behaves differently. Understanding distribution helps when extracting from complex unions or intersections.
type MergeUnion<T> = T extends { a: infer A } ? A : never; type U = { a: string } | { a: number }; type M = MergeUnion<U>; // string | number
Inference distributes across union members. If you need to prevent distribution, wrap the type in a tuple: T extends [infer U] ? ... — remembering this trick helps control behavior. For broader patterns with union and intersection types, see: Union Types: Allowing a Variable to Be One of Several Types and Intersection Types: Combining Multiple Types (Practical Guide).
10) Building a Practical Utility: ExtractProps
A common real-world use is extracting prop types from React-like components or component definitions. This utility infers a props type from a function or class component shape.
// Functional component style type InferProps<T> = T extends (props: infer P) => any ? P : T extends { props: infer P } ? P : never; type FC = (props: { name: string; age?: number }) => any; type P = InferProps<FC>; // { name: string; age?: number }
For class components you might infer from the constructor or static props field. When building apps with React, understanding props typing ties into form handling and patterns; for examples on form handling in React without external libraries, see React Form Handling Without External Libraries — A Beginner's Guide.
Advanced Techniques
Once you are comfortable with the basics, apply these advanced strategies:
- Use helper wrappers to prevent conditional distribution (wrap in a tuple) when you need to infer a whole union as a single value.
- Prefer explicit names for inferred type parameters (e.g., infer Payload) to make types self-documenting.
- Combine infer with template literal types to extract and transform keys, especially when working with namespaced keys (TS 4.1+).
- Use mapped type modifier remapping with as to change property keys while inferring values.
- For performance, keep conditional depth reasonable: highly nested infer chains can make editor tooling slow. Break complex logic into smaller reusable type helpers.
Example performance tip: rather than building one monolithic type that does 6 nested infers, split it into intermediate types and compose them. This also improves readability.
Best Practices & Common Pitfalls
Do:
- Use meaningful inferred variable names in comments to document purpose.
- Split complex types into readable parts.
- Add explicit fallbacks (never or unknown) where appropriate.
Don't:
- Overuse infer for readability gains; sometimes a small manual type is clearer.
- Rely on inference for runtime validation — TypeScript types are erased at runtime.
- Assume distribution always helps; it can produce unexpected unions. Use tuple-wrapping to prevent distribution when needed.
Troubleshooting tips:
- If an inferred type is never, check that the conditional branch actually matches the input shape. Use narrowed test types in separate steps.
- For editor slowness, simplify or split types and update TypeScript to the latest stable release.
When working with classes and access control, be aware of modifiers like public/private/protected — they affect how you extract properties from instances. For guidance on access levels across languages, see Access Modifiers: public, private, and protected — An In-Depth Tutorial.
Real-World Applications
- Event systems: infer payload types for typed dispatchers and handlers, eliminating switch-case type assertions.
- API response shaping: extract nested result types from server responses to feed typed clients.
- Form libraries: infer field value shapes from definitions and build typed validation pipelines. See practical form patterns here: React Form Handling Without External Libraries — A Beginner's Guide.
- Utilities for class-based DI: infer constructor parameters and instance types to wire up factories. If you design class hierarchies, our guide to Class Inheritance: Extending Classes in TypeScript and Abstract Classes: Defining Base Classes with Abstract Members can be helpful.
For frontend projects, inferred types can improve accessibility and performance and integrate well with broader tooling like performance auditing and component design systems. See related guides on web performance and accessibility: Web Performance Optimization — Complete Guide for Advanced Developers and Web Accessibility Implementation Checklist for Intermediate Developers.
Conclusion & Next Steps
Using infer with objects in conditional types empowers you to write concise, reusable, and type-safe utilities that simplify many real-world TypeScript problems. Start by trying small extraction patterns in your codebase, then refactor to reusable helpers. Next steps: explore mapped type modifiers, template literal types, and advanced union handling to expand these patterns into a full utility library. For deeper reading on mapped types and modifying properties, revisit Advanced Mapped Types: Modifiers (+/- readonly, ?).
Enhanced FAQ
Q: What exactly does infer do inside a conditional type? A: infer declares a type variable inside the true branch of a conditional type to capture a portion of the matched type. For example, T extends { payload: infer P } ? P : never captures the type of payload as P if T has that property. If the conditional test doesn't match, the fallback branch runs.
Q: Does infer work with union types? A: Yes. Conditional types distribute over unions by default. If T is a union, the conditional is applied to each member. That often yields a union of inferred types. If you want to prevent distribution, wrap the checked type in a tuple, e.g., [T] extends [U] ? ... — this treats the union as a single entity.
Q: How do I infer keys for specific value types? A: Use a mapped type that maps keys to either the key or never based on the conditional, then index into the mapped type. Example:
type KeysOfType<T, Match> = { [K in keyof T]: T[K] extends Match ? K : never }[keyof T];
This produces a union of keys whose values extend Match.
Q: Can infer capture multiple values at once? A: Each infer declares a single type variable. You can declare multiple infers by matching a pattern that contains multiple inferred slots, e.g., T extends { a: infer A; b: infer B } ? [A, B] : never.
Q: Are there performance concerns with deep infer chains? A: Yes. Very complex nested conditional types can slow down type checking and editor responsiveness. Break types into smaller helpers, avoid excessive nesting, and keep TypeScript up to date. Also prefer clear, readable types over extremely terse but inscrutable definitions.
Q: How does infer interact with mapped types and modifiers? A: infer focuses on capturing types. When you transform shapes (e.g., unwrapping Promises across a type), combine infer with mapped types to compute new property values. If you need to change optional or readonly modifiers, remap keys using as and apply modifiers via the mapped type syntax. See the advanced mapped types guide for examples: Advanced Mapped Types: Modifiers (+/- readonly, ?).
Q: Can I infer types from class instances and constructors? A: Yes. To extract constructor parameter types use new (...args: infer A) => any in your conditional. To extract instance types, match new () => infer I. These are helpful for factories and DI containers. If you are working with class hierarchies or implementing interfaces, see Implementing Interfaces with Classes and Class Inheritance: Extending Classes in TypeScript for patterns and guidance.
Q: What's a practical example where infer saved a lot of code? A: Imagine a large event system with dozens of action shapes. Using infer-driven PayloadOf<T, Type> avoids writing separate types per action and keeps switch handlers strongly typed. Another example: unwrapping deeply nested API result types using nested infer patterns can avoid duplicating interfaces across layers.
Q: How do I debug complex inferred types?
A: Use smaller intermediate types, create sample types to test using type aliases (e.g., type Test = MyComplex
Q: Where can I read more about complementary subjects? A: To expand what you learn here, check out the articles on mapped types, unions, intersections, and literal types linked throughout this guide. For frontend integrations and usage, explore guides on form handling, web performance, and accessibility such as React Form Handling Without External Libraries — A Beginner's Guide, Web Performance Optimization — Complete Guide for Advanced Developers, and Web Accessibility Implementation Checklist for Intermediate Developers.
If you're building component libraries or web components, our advanced tutorial on web components is a useful complement: Implementing Web Components Without Frameworks — An Advanced Tutorial.