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

Infer operator returning container type instead of applied generic #48772

Closed
kschat opened this issue Apr 19, 2022 · 7 comments
Closed

Infer operator returning container type instead of applied generic #48772

kschat opened this issue Apr 19, 2022 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@kschat
Copy link

kschat commented Apr 19, 2022

Bug Report

πŸ”Ž Search Terms

infer, conditional type, generic, alias

πŸ•— Version & Regression Information

  • This changed between versions v4.1.5 and v4.2.3

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type BaseContext<TConfig> = {
  foo: number;
  config: TConfig;
};

type FullContext<TConfig, TContext> = TContext & BaseContext<TConfig>;

type ExtractAdditionalContext<TContext extends FullContext<any, any>> =
  TContext extends FullContext<any, infer TAdditionalContext>
    ? TAdditionalContext
    : never;

type Config = Record<string, string>;
type AdditionalContext = { customValue: string };

type Context = FullContext<Config, AdditionalContext>;
type ExtractedAdditionalContext = ExtractAdditionalContext<Context>;

declare const additionalContext: AdditionalContext;
const test1: ExtractedAdditionalContext = additionalContext;

πŸ™ Actual behavior

I expected ExtractedAdditionalContext to be AdditionalContext or { customValue: string }, and for test1 to compile without error.

πŸ™‚ Expected behavior

ExtractedAdditionalContext is AdditionalContext & BaseContext<Config> which is the same type as Context. test1 results in a compiler error of:

Type 'AdditionalContext' is not assignable to type 'AdditionalContext & BaseContext<Config>'.
 Type 'AdditionalContext' is missing the following properties from type 'BaseContext<Config>': foo, config

Additional notes

When I remove the config property from BaseContext the issue is resolved.

@RyanCavanaugh
Copy link
Member

Bisected to #42284

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Apr 19, 2022
@RyanCavanaugh
Copy link
Member

If you keep substituting in the expansions of each type and see the operation being performed, this is a correct inference -- as in many cases, there are multiple equally correct inferences that can be made, and as a result of the linked PR we switched from a certain correct inference to a different correct inference. In general it's not safe to assume that inferring onto a T & U will reliably "break apart" a source type which was also constructed through a T & U - this can happen, but is not guaranteed to.

@kschat
Copy link
Author

kschat commented Apr 19, 2022

Thanks for the quick reply! I can appreciate how this is a correct inference, but it's certainly surprising. I'm sure a case exists, but I'm having a hard time imaging a time where I'd want the current result.

I guess it's partially surprising because I wrote FullContext<any, infer TAdditionalContext>, not infer TContext & BaseContext<any>. The syntax suggests that I'm asking the compiler to tell me what was provided to the second generic of the type. Clearly that's not how the infer keyword works, but that begs the question -- can we have that feature? :)

@fatcerberus
Copy link

fatcerberus commented Apr 20, 2022

The syntax suggests that I'm asking the compiler to tell me what was provided to the second generic of the type.

Yeah, inference is structural, not nominal, so the type system is always working backwards from the output of the generic to figure out what should go in the infer position. It's a bit like solving for x in f(x, y) = z given fixed y and z - the solution isn't guaranteed to be unique, depending on what f does with its arguments.

Clearly that's not how the infer keyword works, but that begs the question -- can we have that feature? :)

I'm not a TypeScript team member, but from what I do know about how the type system works I'm going to say probably not - in many cases the compiler doesn't even know which type alias a type came from, so infer always has to be done structurally.

@RyanCavanaugh
Copy link
Member

Nothing's impossible but in general we're not keen to spend time tweaking things in the grey areas of inference - there are always going to be cases where there's a structural inference going on here, so making 6 out of 10 cases work as opposed to 4 out of 10 cases doesn't really change the situation much.

@kschat
Copy link
Author

kschat commented Apr 21, 2022

Yeah, inference is structural, not nominal, so the type system is always working backwards from the output of the generic to figure out what should go in the infer position. It's a bit like solving for x in f(x, y) = z given fixed y and z - the solution isn't guaranteed to be unique, depending on what f does with its arguments.

Thanks, I was missing this context and it explains why this is so difficult.

Nothing's impossible but in general we're not keen to spend time tweaking things in the grey areas of inference

That's reasonable and I understand why the team takes that position. I'll close this issue, thanks for you time.

@kschat kschat closed this as completed Apr 21, 2022
@kschat
Copy link
Author

kschat commented Apr 21, 2022

@dpchamps came up with a good solution to my problem that I'll post here for others. Instead of inferring the generic, we can omit the keys in BaseContext from FullContext. This can omit too much if AdditionalContext contains keys that are also in BaseContext, but that's not a problem in my case. Here's a working example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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

3 participants