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

Type error when using generic function result as argument, but not when assigned to temp variable first #35339

Closed
fatcerberus opened this issue Nov 25, 2019 · 10 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@fatcerberus
Copy link

Issue originally discovered by @eyelidlessness on Gitter.

TypeScript Version: 3.8.0-dev.20191123

Search Terms: generic returntype

Code

declare const foo: <
    R1,
    F1 extends () => R1,
    R2,
    F2 extends (input: ReturnType<F1>) => R2
>(
    f1: F1,
    f2: F2
) => any

const f1 = () => ({ a: 'a' })
const f2 = (input: { a: string }) => ({ b: 'b' })

foo(f1, f2) // Good

const makeF1 = () => f1;

foo(makeF1(), f2) // Good

const makeF1p = <T>(t: T) => f1;

foo(
    makeF1p(''),
    f2 // Error WHY :(
);

const made = makeF1p('');

foo(
    made, // This is fine
    f2
)

Expected behavior:

Both foo() calls should compile as the only difference is whether the result of a function call is assigned to a temporary first.

Actual behavior:

The first call to foo(), where makeF1p() is called inline, produces a type error at compile time. The second, where the return value of makeF1p() is assigned to temp variable first, compiles fine. The error is:

Argument of type '(input: { a: string; }) => { b: string; }' is not assignable to parameter of type '(input: unknown) => unknown'.
  Types of parameters 'input' and 'input' are incompatible.
    Type 'unknown' is not assignable to type '{ a: string; }'.

It almost seems like the type parameter inference machinery is leaking outside the scope of the call expression or something? It's weird, at any rate.

Playground Link:

https://www.typescriptlang.org/play/?ts=3.8.0-dev.20191123&ssl=1&ssc=1&pln=33&pc=1#code/CYUwxgNghgTiAEYD2A7AzgF3gMyUgXPADwBQ858ASgIwA0ZFAYtfCAB4YgrBrwAUASngBeAHxU6DcpQBM9CvEYzWHLj34BLFAAcArhkKUQGXTBQAVAJ7aQRZqKFiqMkqL5Sc1Qs3kVsM7xdHcSgUSxISZHQsbBZhfmD+AG94KEIAcih0+ABfAUjUTBxleL4tPQN4FLT4TBgtAHNcxL4UgCMMtuy8iNwkPljaYqEAehH4AHE8YAioooBbKABrEGYRBJFxWIBuXrw+RZXmQSH-UfGppBmC6PhD1eptdaJzN0rzRJ29-o975m0+Ol0gJfOR-PAxvAAKIwGBIGDwADqAAkAJrwfDuAS7G4LKCgdZ-R6A4E4vruBSLUBDSHmAAWGl4jJwWhAHn8JHyQA

Related Issues: I didn't find any, but @jack-williams says #30215 might be the root cause.

@eyelidlessness
Copy link

I don't know if this will help, but I was able to work around this by referencing the type parameter T in makeF1p's return type:

const makeF1p = <T>(t: T) => f1 as Extract<typeof f1 | T, typeof f1>;

@jcalz
Copy link
Contributor

jcalz commented Nov 26, 2019

I think we can remove R1 and R2 and still reproduce with

declare const foo: <F extends () => unknown, G extends (x: ReturnType<F>) => unknown>(
  f: F, g: G) => any

Playground link

@shenjunru
Copy link

Similar case: Playground link

const fn = <A, B>({ a, b, c }: {
    a: () => A;
    b: (a: A) => B;
    c: (a: A, b: B) => any;
}) => {
    const t = a();
    return c(t, b(t));
};

// wrong
fn({
    a: () => 1,
    b: (a) => 1 + a, // prop b type is (a: number) => unknown
    c: (a, b) => a + b, //  arg b is unknown
});

// wrong
fn({
    a: () => 1,
    b: (_) => 1, // prop b type is (a: number) => unknown
    c: (a, b) => a + b, //  arg b is unknown
});

// correct
fn({
    a: () => 1,
    b: () => 1, // prop b type is (a: number) => number
    c: (a, b) => a + b, //  arg b is number
});

@fatcerberus
Copy link
Author

Smaller repro, pretty sure this is the same issue:

declare function foo<T = {}>(): number & T;
declare function bar<T>(x: T): void;

bar(foo());  // T = never

let tmp = foo();  // T = {}
bar(tmp);

Contextual typing seems to be causing chained generic calls to influence each others' type parameters.

@trusktr
Copy link
Contributor

trusktr commented Dec 14, 2019

I'm not sure if this is a bug (regression) or a (new) feature.

It adds one more thing that can make class-factory mixins brittle. F.e., I have to consciously write

import {Mixin, MixinResult, Constructor} from 'lowclass'

function SizeableMixin<T extends Constructor>(Base: T) {
    const _Base = Constructor(Base) // HERE
    const Parent = Observable.mixin(TreeNode.mixin(_Base))

    class Sizeable extends Parent { /* ... */ }

    return Sizeable as MixinResult<typeof Sizeable, T>
}

instead of

import {Mixin, MixinResult, Constructor} from 'lowclass'

function SizeableMixin<T extends Constructor>(Base: T) {
    const Parent = Observable.mixin(TreeNode.mixin(Constructor(Base))) // HERE

    class Sizeable extends Parent { /* ... */ }

    return Sizeable as MixinResult<typeof Sizeable, T>
}

where the definition of Constructor is

export type Constructor<T = object, A extends any[] = any[], Static = {}> = (new (...a: A) => T) & Static

// this is used for type casting in mixins
export function Constructor<T = object, Static = {}>(Ctor: Constructor<any>): Constructor<T> & Static {
	return (Ctor as unknown) as Constructor<T> & Static
}

This issue was not happening before (the second example would work), so it seems to be from some relatively new version of TypeScript within the past few months.

@fatcerberus
Copy link
Author

fatcerberus commented Dec 14, 2019

I’m almost positive it’s a bug. Type inference for a generic call shouldn’t be affected by whether the result is assigned to a variable vs. directly passing it to another generic function.

@trusktr
Copy link
Contributor

trusktr commented Dec 16, 2019

Deleted my previous comments, I was thinking something else. Yeah in @fatcerberus's example above it seems like the generic args for foo should first be inferred, then that should guide inference for bar.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jan 7, 2020
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 7, 2020

This is a fairly normal consequence of having too many type parameters in the absence of unification. We'd need to have an iterative inference algorithm to process this call as expected. The root problem is that there's a candidate via R1 being fed to f2's T via the return type, which is why you only see this when it's in the argument position.

I would rewrite foo to have fewer type parameters, if applicable:

declare const foo: <R1, R2>(f1: () => R1, f2: (input: R1) => R2) => R2

@trusktr
Copy link
Contributor

trusktr commented Jan 7, 2020

What's the performance trade off of adding an "iterative inference algorithm"?

@fatcerberus
Copy link
Author

fatcerberus commented Jan 8, 2020

@RyanCavanaugh

too many type parameters

declare function foo<T = {}>(): number & T;
declare function bar<T>(x: T): void;

bar(foo());  // T = never

let tmp = foo();  // T = {}
bar(tmp);

In this case, there is only a single type parameter involved, but appears to trigger the same issue. Is it actually the same issue, or is this something else? Because I really don't expect the presence--or lack thereof--of an intermediate assignment (which is itself inferred!) to affect the inference of the function call... and yet it does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

7 participants
@shenjunru @jcalz @eyelidlessness @trusktr @fatcerberus @RyanCavanaugh and others