Skip to content

Reverse mapped type with a circular type param sometimes not treated as partially inferrableΒ #48798

Open
@Andarist

Description

@Andarist

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:

  1. methods + functions with arguments are context sensitive and in checkFunctionExpressionOrObjectLiteralMethod they return anyFunctionType
  2. anyFunctionType has ObjectFlags.NonInferrableType on it
  3. this flag is "propagating" and thus is set on the "parent" object
  4. uninstantiatedType for the on property of this argument gets computed to {} (so it's empty~)
  5. and thus it doesn't pass isPartiallyInferableType check when resolving the structure of the reverse mapped type, since the object has ObjectFlags.NonInferrableType on it AND there are no other properties that would be treated as partially inferrable
  6. since no structure is resolved for this mapped type the unknown is returned for the states property
  7. this in turn makes initial property to error because keyof unknown is never

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs InvestigationThis issue needs a team member to investigate its status.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions