Extensive documentation on the specific issue you're encountering is not readily available (the closest reference found is ms/TS#56905). In a broad context, TypeScript struggles with executing various type operations on generic types efficiently. It often delays evaluation (resulting in valid assignments being rejected) or oversimplifies the generic type parameter to its constraint (leading to unsafe scenarios).
In principle, it should hold that (X & Y)[K]
can be assigned to X[K]
(provided that K
is a recognized key of
X</code). When dealing with specific <code>X
,
Y
, and
K
types, TypeScript can validate this by fully evaluating the
intersection and the
indexed access. However, if
K
happens to be a generic type, TypeScript fails to verify it properly and defers the assessment, treating it as an obscure entity:
const selectedObject = obj[key];
// ^? const selectedObject: Obj<Type>[T]
The connection between each key T
and the associated type InnerObject<T>
isn't directly encoded in the type Obj<Type>
; rather, it's indirectly represented through the intersection. Consequently, TypeScript overlooks this link:
const selectedObject: InnerObject<T> | undefined = obj[key]; // error!
This error signifies what would occur if you tried assigning obj[key]
to InnerObject<K>
for any unrelated generic K
. The only safe assignment scenario is when obj[key]
matches InnerObject<K>
for every possible K
, which gives rise to "cross-assignment" complications.
To ensure successful assignment, there is a need to assist TypeScript in perceiving the operation more straightforwardly. If you possess a mapped type structured as {[P in K]: F<P>}
and utilize the key K
to index into it, TypeScript will recognize it as assignable to F<K>
. This aligns with the concept of a distributive object type elaborated in microsoft/TypeScript#47109. Given that the mandatory mapped type is encompassed within the Obj<K>
definition, widening any Obj<K>
to that segment should be secure.
The suggested course of action is as follows:
function getInnerObject<T extends Type>(key: T) {
const o: { [P in Type]?: InnerObject<P> } = obj; // widen
const selectedObj: InnerObject<T> | undefined = o[key] // index into widened thing
const defaultObj = obj['one']
}
Initially broaden obj
from Obj<Type>
to o
having the type
{[P in Type]?: InnerObject<P>}
. Such expansion is permissible because it aligns with one element of the intersection perfectly, exploiting TypeScript's leniency toward widening from
X & Y
to
X
. Then proceed to access this mapped type using the generic
key
represented by
T
, thereby enabling TypeScript to approve the resultant value as assignably compatible with
InnerObject<T> | undefined
.
Note that retaining the obj
value for future use is crucial. The variable o
served solely to conduct widening securely. Achieving this without an additional variable is feasible using the safe widening notion x satisfies T as T
, where satisfies
verifies the type and as
widens it:
type OT = { [P in Type]?: InnerObject<P> };
const selectedObj: InnerObject<T> | undefined = (obj satisfies OT as OT)[key];
Playground link to code