Repository navigation
Reverse mapped type with a circular type param sometimes not treated as partially inferrable #48798
Description
Activity
andrewbranch commented
on Apr 25, 2022 MemberMore actionsAnders Hejlsberg (@ahejlsberg) thoughts on this one?
The example works as expected if you remove the circular constraint on
TConfig:const createMachine = <TConfig extends object>( _config: InferNarrowestObject<TConfig> ): void => {};
So, this tells me that the type inferred for
TConfigfailed to be assignable toConfig<TConfig>and inference therefore pickedConfig<TConfig>as the inferred type. I'm not sure why the assignment check fails, but could well be because the types aren't right.Andarist commented
on Apr 27, 2022 ContributorAuthorMore actionsThe goal of this kind of type is to get "access" to the type of the argument and remap it on our own, introduce relations between certain keys etc as the input acts as a schema. In this particular example, I'm restricting
TArgument['initial']to be of typekeyof TArgument["states"]. This isn't something that is documented or anything - it's just a technique discovered by other people (I've learned it from them).I'm not sure if we can conclude here that the inferred type failed to be assignable to the constraint, or maybe it was - but then the question/problem would be why an unassignable type was inferred there.
Even if remove some unusual stuff from this repro case then it still leaves me with confusing results:
type Config<TGlobal extends { states: unknown }> = { initial: keyof TGlobal["states"]; states: { [StateKey in keyof TGlobal["states"]]: { on?: Record<string, () => string>; }; }; }; const createMachine = <TConfig extends Config<TConfig>>( _config: TConfig ): void => {}; createMachine({ initial: 'pending', states: { pending: { on: { done() { return ''; }, // errors on initial property because the inferred type is Config<unknown> // done: () => { return ''; }, // just works // done: () => { return 42 } // errors correctly because the return type doesnt match }, }, }, });
And if we take a look at one of the mentioned workarounds for the original repro (see here), it is quite surprising that a random~ added property just "fixes" this. If the problem would be in the type then I would expect this extra property to just error in the same way - but according to my debugging session it's just related to how inference is currently implemented in relation to context sensitive functions. Just note that in this repro with an extra property this is still an context sensitive function - the extra property doesn't really change that trait. It just makes the parent object "partially inferrable" and then the inference algorithm is able to proceed further and accept this method there.
Note that if I add more types to the
onproperty then I can further validate that TS sees my types and is able to type-check using them (as long as I have that extra property that makes the thing partially inferrable), see this playground- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Apr 27, 2022 andrewbranch commented
on Apr 27, 2022 MemberMore actionsSetting 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.
Andarist commented
on Apr 27, 2022 ContributorAuthorMore actionsAndrew Branch (@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.
Reacted by Andrew Branch
Bug Report
🔎 Search Terms
reverse mapped types, circular type, partially inferrable
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
Inferred signature is:
🙂 Expected behavior
states: unknownshouldn'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: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"
thistype 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:
checkFunctionExpressionOrObjectLiteralMethodthey returnanyFunctionTypeanyFunctionTypehasObjectFlags.NonInferrableTypeon ituninstantiatedTypefor theonproperty of this argument gets computed to{}(so it's empty~)isPartiallyInferableTypecheck when resolving the structure of the reverse mapped type, since the object hasObjectFlags.NonInferrableTypeon it AND there are no other properties that would be treated as partially inferrableunknownis returned for thestatespropertyinitialproperty to error becausekeyof unknownisneverwhile 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
onproperty 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