Description
Bug 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 #40439