What this usually means
Mapped types that behave unexpectedly are almost always victims of conditional type distribution. When a generic parameter T is used in a conditional type within a mapped type, TypeScript distributes the conditional over each member of a union. If the conditional is not wrapped or the distribution is unwanted, it can collapse to 'never' for each constituent. Another common cause is the difference between homomorphic and non-homomorphic mapped types: when you use an 'as' clause with key remapping, the mapping becomes non-homomorphic and loses the optional/readonly modifiers, which can cause 'undefined' or 'never' to appear in unexpected places. Key remapping with template literal types will not filter keys unless you explicitly exclude them — missing an 'as never' branch is a frequent mistake.
The first ten minutes — establish facts before touching code.
- 1Check if the mapped type uses 'in keyof T' with a generic T — add 'extends string | number | symbol' constraint to T if needed
- 2Test the mapped type with a concrete union literal (e.g., 'type Concrete = {a: string}' ) to see if the issue is generic-specific
- 3Wrap any conditional type inside the mapped value in a '[]' tuple to prevent distribution: '[T] extends [U] ? ... : ...'
- 4Inspect the resulting type with 'type Result = MyMapped<SomeType>' and hover over Result — look for 'never' or 'unknown'
- 5Remove the 'as' clause from the mapped type and see if the problem disappears to isolate key remapping issues
- 6Run 'tsc --noEmit --extendedDiagnostics' and look for 'Excessive stack depth' warnings
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe mapped type definition itself — especially the 'as' clause and the value expression
- searchThe generic constraint on the type parameter (e.g., 'T extends Record<string, any>')
- searchAny conditional types used inside the mapped value — check for unintended distribution
- searchThe call site where the mapped type is instantiated with a concrete type
- searchtsconfig.json — 'strictNullChecks' and 'exactOptionalPropertyTypes' can change behavior
- searchTypeScript playground with the exact code to get the expanded type output
Practical causes, not theory. These are the things you will actually find.
- warningConditional type distribution inside mapped type: 'T extends U ? X : Y' distributes over union members causing 'never'
- warningKey remapping with 'as' clause that inadvertently excludes all keys (e.g., 'as never' when condition fails)
- warningGeneric constraint too loose: T extends any allows 'unknown' to slip in and map to 'never'
- warningMissing 'in' keyword before 'keyof' — 'type Mapped<T> = { [P: keyof T]: T[P] }' is invalid syntax
- warningUsing 'keyof T' as a type directly instead of iterating over its members in the mapped type
- warningHomomorphic mapped type loses modifiers when 'as' clause is added (TS 4.1+ behavior change)
Concrete fix directions. Pick the one that matches your root cause.
- buildWrap conditional types in a tuple to prevent distribution: '[T] extends [U] ? X : Y'
- buildAdd a 'never' filter in key remapping: 'as T extends SomeConstraint ? P : never'
- buildConstrain generic parameter: 'T extends Record<string, any>' instead of 'T extends object'
- buildUse 'RemoveIndexSignature<T>' utility or explicitly filter keys with 'as' clause to exclude index signatures
- buildReplace 'in keyof T' with 'in keyof T & string' when keys are only strings to narrow keyof
- buildIf using 'as' clause, add '& {}' to preserve homomorphic behavior: 'type Mapped<T> = { [P in keyof T as NewKey]: T[P] } & {}'
A fix you cannot prove is a guess. Close the loop.
- verifiedTest with a concrete type that has optional, readonly, and index signature properties
- verifiedUse 'type Expand<T> = { [K in keyof T]: T[K] }' to expand and inspect the resulting type
- verifiedCheck if the original issue (e.g., 'never' properties) is gone by hovering over the new type
- verifiedRun 'tsc --noEmit' on a test file that exercises the mapped type with various inputs
- verifiedVerify that conditional branches produce the expected types by extracting them into standalone type aliases
- verifiedWrite a unit test using 'expectType<Expected, Actual>()' from third-party libraries like 'tsd'
Things that make this bug worse or harder to find.
- warningAdding 'extends never' in conditional without understanding distribution — this can suppress errors but mask the bug
- warningUsing 'any' to work around type errors — it will propagate and make the mapped type return 'any' silently
- warningOverusing 'as' clause to coerce types without proper narrowing — leads to runtime casting issues
- warningIgnoring the order of evaluation: TypeScript evaluates mapped types lazily, so errors may not surface until instantiation
- warningAssuming that 'keyof T' returns only string literals — it can include 'number' and 'symbol' if not constrained
Mapped Type Collapsing to 'never' During API Response Transformation
Timeline
- 09:15User reports that API response types are showing as 'never' in the Redux state
- 09:30I inspect the mapped type 'ApiResponseToState<T>' used in the store
- 09:45Hover over the type shows 'type Result = never' for a known API endpoint
- 10:00Check the mapped type: it uses 'in keyof T' with an 'as' clause to rename keys
- 10:15Remove the 'as' clause — type works correctly now, so issue is key remapping
- 10:25Realize the 'as' clause condition is too strict: 'as P extends 'data_' ? P : never' drops all keys because 'data_' is not a string literal
- 10:35Fix: change condition to 'as P extends `data_${string}` ? P : never' and wrap in tuple to prevent distribution
- 10:45Verify with multiple API types — all work. Commit fix.
At 09:15, a user reported that after a recent refactor, the Redux state for several API endpoints was showing as 'never' when they tried to access properties. This was critical because it broke all selectors that depended on that state. I jumped into the codebase to find the mapped type 'ApiResponseToState<T>' that transformed the API response shape into the Redux state shape.
I opened the type definition and saw it used a mapped type with key remapping: 'type ApiResponseToState<T> = { [K in keyof T as T[K] extends ApiResponse ? `data_${K & string}` : never]: Transform<T[K]> }'. The intention was to prefix keys that had an 'ApiResponse' value type. However, when I tested with a concrete type like '{ user: ApiResponse<...> }', the result was 'never'. I removed the 'as' clause and the type worked, so I knew the problem was in the key remapping.
After staring at the condition for a while, I realized the error: 'T[K] extends ApiResponse' was distributing over the union members because 'T[K]' could be a union. The condition evaluated to 'never' for each member that didn't match, and because the distribution caused it to collapse to 'never' for the entire key. The fix was to wrap the conditional in a tuple: '[T[K]] extends [ApiResponse]'. Also, the template literal needed to be '`data_${K & string}`' to ensure K was a string. After applying these fixes, the mapped type worked correctly, and I verified with several API types.
Root cause
Conditional type distribution inside key remapping — the 'extends' check distributed over union members of T[K], returning 'never' for each, causing the entire key to be dropped.
The fix
Wrap the conditional in a tuple: '[T[K]] extends [ApiResponse] ? `data_${K & string}` : never' and ensure K is narrowed to string with 'K & string'.
The lesson
Always guard against conditional type distribution inside mapped types, especially in key remapping. Use tuple wrappers and explicit constraints to avoid silent 'never' collapses.
Conditional types distribute over union types when the checked type is a bare type parameter. Inside a mapped type, the iteration variable (e.g., 'P' in 'P in keyof T') is a type parameter, and if you use 'T[P]' in a conditional, it may be a union. TypeScript distributes the conditional over each union member, and if any member fails the check, the result includes 'never'. Union with 'never' collapses to 'never' for that branch.
Example: 'type Foo<T> = { [K in keyof T]: T[K] extends string ? 'yes' : 'no' }'. If 'T[K]' is 'string | number', the conditional distributes: 'string extends string ? 'yes' : 'no' | number extends string ? 'yes' : 'no'' yields ''yes' | 'no''. But if one branch is 'never', it disappears. The fix is to use a non-distributive conditional: '[T[K]] extends [string] ? 'yes' : 'no''.
In TypeScript 4.1+, the 'as' clause in mapped types allows renaming keys. However, adding an 'as' clause changes the mapping from homomorphic to non-homomorphic. Homomorphic mapped types preserve modifiers like optional and readonly from the input type. Non-homomorphic ones produce properties with default modifiers (required and mutable). This can cause issues when you expect optional properties to remain optional.
Additionally, the 'as' clause can filter keys by returning 'never' for excluded keys. A common mistake is to use a conditional that returns 'never' for some keys but forget that the condition may never be true due to distribution. Also, using template literal types in the 'as' clause requires careful handling of 'string & K' to ensure the key is a string. If 'K' includes 'number' or 'symbol', template literals may fail.
When a mapped type is complex, it's hard to see the resulting type by hovering. Use an 'Expand' utility: 'type Expand<T> = { [K in keyof T]: T[K] }'. This recursively expands the type, making it readable. For deeper types, use 'DeepExpand' or the 'type-fest' library's 'Simplify'. Sometimes the issue is that the mapped type is too deeply nested, causing TypeScript to bail out and show 'any' or 'unknown'.
You can also use 'tsc --noEmit --extendedDiagnostics' to see if the compiler is hitting recursion limits. If you see 'Type instantiation is excessively deep and possibly infinite', you may need to simplify the mapped type or add 'extends' constraints to break the recursion. Another trick is to extract the mapped type into a helper that uses 'infer' to limit depth.
A mapped type that works with a concrete type may break with a generic because the generic constraint is too loose. For example, 'T extends object' allows 'unknown', which maps to 'never' when you try to access 'keyof unknown'. Always constrain to 'Record<string, any>' or 'Record<PropertyKey, any>' for dictionary-like types. Also, if you use 'in keyof T', ensure 'T' is constrained to a type with known keys; otherwise, 'keyof T' may be 'never', and the mapped type produces an empty object.
Another issue is with index signatures. If your mapped type is intended to work with index signatures, you need to handle them explicitly. Use 'RemoveIndexSignature<T>' to exclude index signatures from the mapped iteration, or use the 'as' clause to filter them out. Otherwise, you may get unexpected 'never' or 'undefined' for properties that come from index signatures.
Frequently asked questions
Why does my mapped type return 'never' for all properties when I use 'as never' in the key remapping?
The 'as never' clause is used to exclude keys. If your condition always evaluates to 'never' (e.g., due to distribution or incorrect type check), all keys are excluded, leaving an empty object type. Check your conditional logic and ensure that for keys you want to keep, you return the key name (or a transformed version) instead of 'never'.
What is the difference between 'in keyof T' and 'in keyof T & string'?
'keyof T' can include 'string', 'number', and 'symbol' keys. Using 'in keyof T & string' narrows the iteration to only string keys. This is useful when you want to use template literal types in the 'as' clause, as they only work with string literal types. Without the intersection, you might get errors if 'T' has numeric or symbol keys.
How can I prevent conditional type distribution inside a mapped type?
Wrap the conditional type in a tuple: '[T] extends [U] ? X : Y' instead of 'T extends U ? X : Y'. The tuple prevents distribution because TypeScript does not distribute over tuple types. Alternatively, use a type parameter that is not a union, but the tuple wrapper is the standard approach.
My mapped type works in isolation but fails when I pass a generic parameter. Why?
The generic parameter may not be constrained enough, or the mapped type relies on some concrete properties that are not guaranteed by the constraint. For example, if you access 'T['id']' without checking that 'id' exists in T, it will fail. Add appropriate constraints like 'T extends { id: string }' or use conditional checks to handle optional keys.
What does the error 'Type instantiation is excessively deep and possibly infinite' mean when dealing with mapped types?
This error occurs when TypeScript's type instantiation stack exceeds a limit. It often happens with recursive mapped types or deeply nested generics. To fix it, simplify the type, break it into smaller pieces, add 'extends' constraints to stop recursion, or use 'type-fest' utilities like 'Simplify' to limit depth. You can also increase the recursion limit in tsconfig.json with '--maxNodeModuleJsDepth' but that's not recommended for production.