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

Recursive generics in function parameters are inferred as {}, and produce funky Intellisense #17522

Closed
SamPruden opened this issue Jul 31, 2017 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@SamPruden
Copy link

SamPruden commented Jul 31, 2017

TypeScript Version: 2.4.2

Code

function build<O extends Options<O>>(options: O) {}

interface Options<TSelf> {
    data?: {};
    processSelf(self: TSelf);
}

build({
    data: { hello: "world" },
    processSelf(self) {
        self.data.hello;
    }
});

Expected behavior:
The inferred type of O should be the type of the options object passed in.
self should be of type O.
self.data.hello should be accessible within the processSelf function.

If this is not supposed to work, I would expect O extends Options<O> to be an error.

Actual behavior:
The inferred type of O is Options<{}>.
self is of type {}.
self.data.hello is therefore not accessible.

Additionally, the Intellisense VSCode is providing for this seems to be wrong, or at least confusing. If this is unrelated then I'm happy to make a separate issue for that and remove it from here.

As these screenshots show, self is inferred as {} but it allows access to self.data. self.data is given type any according to the tooltip, but the error at .hello is

Property 'hello' does not exist on type '{}'.

When I explicitly specify self: {}, self.data becomes an error.

image
image

Real scenario:
In my real scenario Options has many more optional properties, and I want self to have only the properties specified, not every optional property. There are also many optional functions with the signature of processSelf(self: TSelf) and they do need to be part of the same options object.

@DanielRosenwasser
Copy link
Member

The problem here is that in order to get the type O in your call to build, you need to get the type of processSelf. In the body of processSelf, the properties of O are requested when using self. When that happens now, TypeScript says "stop making inferences for O" because it needs a concrete type to operate on.

But since there are no completed inferences for O while checking the inside of the object, TypeScript falls back to the type parameter's constraint (which is implicitly {}).

@ahejlsberg worked on a way to prevent this when inferring the type of this in object literals, but at this point I don't know how we'd generalize to other parameters.

@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Aug 1, 2017
@DanielRosenwasser DanielRosenwasser added this to the Future milestone Aug 1, 2017
@mhegazy mhegazy removed this from the Future milestone Aug 31, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Nov 6, 2017

This seems to be doable with some slight changes:

declare function build<D>(options: OptionsWithData<D>): void;
declare function build(options: Options): void;

interface Options {
    processSelf(self: Options): void;
}

interface OptionsWithData<TData> extends Options {
    data: TData
    processSelf(self: OptionsWithData<TData>): void;
}

build({
    data: { hello: "world" },
    processSelf(self) {
        self.data.hello;
    }
});

@mhegazy mhegazy added Working as Intended The behavior described is the intended behavior; this is not a bug and removed In Discussion Not yet reached consensus Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Nov 6, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Nov 6, 2017

it is important to note that the constraint is not an inference location. all what O extends Options<O> does is checks that whatever the compiler inferred for O is assignable to Options<O>

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants