Reverse mapped type with a circular type param sometimes not treated as partially inferrable
See original GitHub issueBug Report
š Search Terms
reverse mapped types, circular type, partially inferrable
āÆ Playground Link
Playground link with relevant code
š» Code
type AnyFunction = (...args: any[]) => any;
type InferNarrowest<T> = T extends any
? T extends AnyFunction
? T
: T extends object
? InferNarrowestObject<T>
: T
: never;
type InferNarrowestObject<T> = {
readonly [K in keyof T]: InferNarrowest<T[K]>;
};
type Config<TGlobal, TState = Prop<TGlobal, "states">> = {
states: {
[StateKey in keyof TState]: {
on?: {};
};
};
} & {
initial: keyof TState;
};
type Prop<T, K> = K extends keyof T ? T[K] : never;
const createMachine = <TConfig extends Config<TConfig>>(
_config: InferNarrowestObject<TConfig>
): void => {};
createMachine({
initial: "pending",
states: {
pending: {
on: {
done() {
return "noData";
},
},
},
},
});
š Actual behavior
Inferred signature is:
const createMachine: <Config<{
initial: "pending";
states: unknown;
}, unknown>>(_config: InferNarrowestObject<Config<{
initial: "pending";
states: unknown;
}, unknown>>) => void
š Expected behavior
states: unknown
shouldnāt be inferred here, the reverse mapped type should be able to infer an object literal type~. The type param should be inferred as something like:
const createMachine: <{
initial: "pending";
states: {
pending: {
on: {
done: () => "noData";
};
};
};
}>(_config: InferNarrowestObject<{
initial: "pending";
states: {
pending: {
on: {
done: () => "noData";
};
};
};
}>) => void
We can simplify the repro a little bit, but it will then yield an error at a different position and I canāt verify right now if the underlying issue is exactly the same in this case (although from what it looks like the root cause is super similar): TS playground without conditional type applied at the argument position
Note that we can fix both playground by using an arrow function instead of a method (probably related to a possible āhiddenā this
type param that makes this context sensitive in the case of a method).
The first one can be fixed by adding a dummy property to the object containing a method (this makes the object partially inferrable): TS playground with a dummy property added
Especially given that a dummy property fixes the problem it looks like a weird design limitations.
What Iāve learned when debugging this:
- methods + functions with arguments are context sensitive and in
checkFunctionExpressionOrObjectLiteralMethod
they returnanyFunctionType
anyFunctionType
hasObjectFlags.NonInferrableType
on it- this flag is āpropagatingā and thus is set on the āparentā object
uninstantiatedType
for theon
property of this argument gets computed to{}
(so itās empty~)- and thus it doesnāt pass
isPartiallyInferableType
check when resolving the structure of the reverse mapped type, since the object hasObjectFlags.NonInferrableType
on it AND there are no other properties that would be treated as partially inferrable - since no structure is resolved for this mapped type the
unknown
is returned for thestates
property - this in turn makes
initial
property to error becausekeyof unknown
isnever
while a workaround is āknownā here (weāve learned this hard way though)~ the whole thing still has some problems because we canāt provide a param type for the arrow functions contained in on
property because once we add any unannotated params to an arrow function it becomes context-sensitive and results in a similar problem:
TS playground with context-sensitive arrow function
Related to https://github.com/microsoft/TypeScript/issues/40439
Issue Analytics
- State:
- Created a year ago
- Reactions:1
- Comments:5 (5 by maintainers)
Top GitHub Comments
@andrewbranch happy to hop on a call if you would be up for it - Iāve spent a few hours investigating this in the debugger so I have a good understanding of what happens and where.
Setting a milestone to look at this because Iām intrigued, but there is a significant chance I wonāt be able to get to it in that timeframe or I wonāt find anything actionable.