Skip to content

Type inference of generics does not work only when callback takes argument #29123

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

Open
ryym opened this issue Dec 22, 2018 · 3 comments
Open
Labels
Bug A bug in TypeScript
Milestone

Comments

@ryym
Copy link

ryym commented Dec 22, 2018

I encountered a strange behavior of type inference about the generic arguments and intersection type. The error described below occurs only when the strictFunctionTypes option is enabled.


TypeScript Version:

Version 3.3.0-dev.20181221

Search Terms:

  • strictFunctionTypes
  • argument intersection type
  • generic argument
  • generic intersection type

Code

tsconfig.json:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "target": "es6",
    "module":"commonjs",
    "strictFunctionTypes": true,
    "outDir": "dist"
  },
  "include": ["*.ts"]
}

source:

const run = <P1, P2>(
  makeProps1: (s: string) => P1,
  makeProps2: () => P2,
  useBoth: (props: P1 & P2) => void
) => {};

const foo = (props: { n: number; b: boolean }) => {};

// This works as expected.
run(() => ({ n: 1 }), () => ({ b: true }), foo);

// But if the first argument function takes a parameter `s`, it results in a compile error.
run(s => ({ n: 1 }), () => ({ b: true }), foo);

Expected behavior:

The type of run is inferred correctly from run(s => ({ n: 1 }), () => ({ b: true }), foo) as below and the code compiles.

const run: <{ n: number; }, { b: boolean; }>(makeProps1: (s: string) => { n: number; }, makeProps2: () => { b: boolean; }, useBoth: (props: { n: number; } & { b: boolean; }) => void) => void

This type is generated from run(() => ({ n: 1 }), () => ({ b: true }), foo). I copied it from Playground's tooltip.

Actual behavior:

It results in the compile error.

output of npx tsc:

repro.ts:9:43 - error TS2345: Argument of type '(props: { n: number; b: boolean; }) => void' is not assignable to parameter of type '(props: { b: boolean; }) => void'.                                  
  Types of parameters 'props' and 'props' are incompatible.
    Property 'n' is missing in type '{ b: boolean; }' but required in type '{ n: number; b: boolean; }'.                                                                                                 

9 run(s => ({ n: 1 }), () => ({ b: true }), foo);
                                            ~~~

  repro.ts:7:23
    7 const foo = (props: { n: number; b: boolean }) => {};
                            ~
    'n' is declared here.


Found 1 error.

This is because the return type of makeProps1 is inferred as {} instead of { n: number; }.

const run: <{}, { b: boolean; }>(makeProps1: (s: string) => {}, makeProps2: () => { b: boolean; }, useBoth: (props: { b: boolean; }) => void) => void

Playground Link:

Here. Note that this problem occurs only when the strictFunctionTypes option is enabled.

Related Issues:

I could not find.

Other Investigation:

In the code below, the type inference works as expected:

const run2 = <P1, P2>(
  makeProps1: (s: string) => P1,
  makeProps2: () => P2,
  useProps1: (p: P1) => void,
  useProps2: (p: P2) => void
) => {};

// The first argument function takes a parameter `s` but it does not change the type inference behavior.
run2(
  s => ({ n: 1 }),
  () => ({ b: true }),
  (p1: { n: number }) => {},
  (p2: { b: boolean }) => {}
);

So it seems that the error only occurs when the generic types (P1, P2) are used as intersection type.

@weswigham weswigham added the Bug A bug in TypeScript label Dec 25, 2018
@aleclarson
Copy link

Did you try StackOverflow first?

Here's a workaround:

const run = <P1, P2, P3 extends P1 & P2>(
  makeProps1: (s: string) => P1,
  makeProps2: () => P2,
  useBoth: (props: P3) => void
) => {};

const foo = (props: { n: number; b: boolean }) => {};

run(() => ({ n: 1 }), () => ({ b: true }), foo);
run(s => ({ n: 1 }), () => ({ b: true }), foo);

@weswigham Does the TS team actually consider this an inference bug? It would definitely be nice to not need P3 in the workaround above.

@weswigham
Copy link
Member

mmmm labeled it as a bug because I personally don't believe the strictFunctionTypes flag should affect inference results, for consistency. It might end up getting recategorized when we look at it in depth, but that's why I flagged it as "bug" initially.

@ryym
Copy link
Author

ryym commented Jan 14, 2019

@aleclarson

Did you try StackOverflow first?

No, because I think this inconsistent behavior should be reported as an issue even if this is not a bug.

But I didn't come up with your workaround. Thank you!

A small downside of the workaround is that the props of foo can contain any extra properties.

const run = <P1, P2, P3 extends P1 & P2>(
  makeProps1: (s: string) => P1,
  makeProps2: () => P2,
  useBoth: (props: P3) => void
) => {};

const foo = (props: { n: number; b: boolean, s: string }) => {};

// This compiles. But `s` in foo's props will be undefined in this case.
run(() => ({ n: 1 }), () => ({ b: true }), foo);

However it does not compile if the type inference works as expected.

const run = <P1, P2>(
  makeProps1: (s: string) => P1,
  makeProps2: () => P2,
  useBoth: (props: P1 & P2) => void
) => {};

const foo = (props: { n: number; b: boolean, s: string }) => {};

// Compile error as expected: `Types of parameters 'props' and 'props' are incompatible`
run(() => ({ n: 1 }), () => ({ b: true }), foo);

So it would be nice if this type inference works well with the strictFunctionTypes option.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants