Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Params inferred as unknown when they contain methods with recursive param types #51578

Closed
lynndylanhurley opened this issue Nov 17, 2022 · 7 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@lynndylanhurley
Copy link

Bug Report

In this example using a recursive generic type, the b prop of the second createFoo is inferred to be unknown.

type FooConfig<T> = {
  [K in keyof T]: {
    [P in keyof T[K]]: number | ((f: Foo<T>) => number);
  }
}

type Foo<T> = {
  [K in keyof T]: {
    [P in keyof T[K]]: number;
  }
};

const createFoo = <T, TFoo = Foo<T>>(args: FooConfig<T>): TFoo =>  {
  // code...
}

const foo = createFoo({
  a: {
    i: 1,
  },
  b: {
    ii: () => 1, // <-- no function arg, works as expected
  }
});

// works as intended
console.log('@-->foo', foo.b.ii);

const foo2 = createFoo({
  a: {
    i: 1,
  },
  b: {
    ii: (f) => f.a.i += 1, // <-- only difference is that this method has an argument
    // iii: 3 // <-- uncomment this line and it works
  }
});

// type error when function argument is present in config
console.log('@-->foo2', foo2.b.ii);

This example has two instances of the createFoo method. The first call is not using a function argument for the b.ii param and everything works as intended.

In the 2nd createFoo call, the b.ii param takes a method argument that is a recursive reference to the createFoo method argument itself.

The recursive reference type itself works as expected (i.e. the f in b.ii is typed correctly)

But in the resulting output, references to foo2.b.ii cause a type error.

Strangely, if I add a b.iii param as either a number or a () => number type (i.e. not using the recursive reference), then the foo2.b.ii type error is resolved.

🔎 Search Terms

  • recursive unknown

🕗 Version & Regression Information

This bug is present in every version of TypeScript available in the TS Playground.

⏯ Playground Link

Playground link with relevant code

💻 Code

See above

🙁 Actual behavior

The foo2 instance results in the following .d.ts:

declare const foo2: Foo<{
    a: {
        i: unknown;
    };
    b: unknown;
}>;

🙂 Expected behavior

The foo2 instance should result in something like this:

declare const foo2: Foo<{
    a: {
        i: unknown;
    };
    b: {
        ii: unknown;
    };
}>;
@lynndylanhurley
Copy link
Author

I've tried for several days to get help on the typescript discord channel, but I haven't received a single response. So I'm wondering if this is maybe a bug.

Also posted this to StackOverflow.

@RyanCavanaugh
Copy link
Member

See #47599

@lynndylanhurley
Copy link
Author

Thank you @RyanCavanaugh. That issue describes my situation perfectly.

Am I correct in assuming that this input type should be inferrable?

const foo2 = createFoo({
  a: {
    i: 1,
  },
  b: {
    ii: (f) => 1 // f is a recursive reference to the base createFoo method param
  }
});

Or am I trying to do something that goes against the design goals of TypeScript?

@RyanCavanaugh
Copy link
Member

I don't see any theoretical blocker to inferring that.

That said, I think in general an algorithm that would be capable inferring this would be one that would do an unbounded number of inference passes. Our inference algorithm uses a fixed number of passes for performance reasons and as a result can't always infer cases like this. We're not against tweaking it to infer more, but this would need to be weighed against perf cost.

Basically TL;DR not a defect (unknown inference is sound) but if a good PR existed to fix this case, we'd take it.

@lynndylanhurley
Copy link
Author

Thanks @RyanCavanaugh ! With that information, I think I can find another approach to my design. I really appreciate your help 🙏

@lynndylanhurley
Copy link
Author

One more question about this. Using this example:

const foo2 = createFoo({
  a: {
    i: 1,
  },
  b: {
    ii: (f) => 1 // f is a recursive reference to the base createFoo method param
  }
});

I understand why the type of b.ii is unknown. But shouldn't the system be able to infer something like this?

declare const foo2: Foo<{
    a: {
        i: unknown;
    };
    b: {
        ii: unknown;
    };
}>;

Currently the entire b object is inferred as unknown (i.e. no b.ii key), and it's making it difficult for me to find a workaround:

declare const foo2: Foo<{
    a: {
        i: unknown;
    };
    b: unknown;
}>;

Is there another way to structure this so that the nested keys are inferred correctly?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Nov 18, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

3 participants