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

Generic function does not accept argument of type with union and intersection #44315

Open
turtleflyer opened this issue May 28, 2021 · 4 comments
Labels
Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@turtleflyer
Copy link

turtleflyer commented May 28, 2021

Bug Report

πŸ”Ž Search Terms

generic function union intersection type optional property

πŸ•— Version & Regression Information

  • This changed between versions 3.6.0-dev.20190723 and version 3.6.0-dev.20190724

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type A = ({ a: any } | { b: any }) & { c?: any };

declare function testA<Param extends A>(param: Param & A): Param;

declare const s1: A;

const a1 = testA(s1);
/**
 * Argument of type 'A' is not assignable to parameter of type '{ b: any; } & A'.
 *   Type '{ a: any; } & { c?: any; }' is not assignable to type '{ b: any; } & A'.
 *     Type '{ a: any; } & { c?: any; }' is not assignable to type '{ b: any; } & { a: any; } & { c?: any; }'.
 *       Property 'b' is missing in type '{ a: any; } & { c?: any; }' but required in type '{ b: any; }'.(2345)
 */

type B = ({ a: any } | { b: any }) & { c: any };

declare const s2: B;

const a2 = testA(s2); // Type of result is A but must be B

πŸ™ Actual behavior

The line const a1 = testA(s1); gives an error:

Argument of type 'A' is not assignable to parameter of type '{ b: any; } & A'.
  Type '{ a: any; } & { c?: any; }' is not assignable to type '{ b: any; } & A'.
    Type '{ a: any; } & { c?: any; }' is not assignable to type '{ b: any; } & { a: any; } & { c?: any; }'.
      Property 'b' is missing in type '{ a: any; } & { c?: any; }' but required in type '{ b: any; }'.(2345)

In the line const a2 = testA(s2); the typescript compiler infers the constanta a2 has type A

πŸ™‚ Expected behavior

The variable s1 has the type A that means function testA<Param extends A>(param: Param & A): Param accepts s1. The parameter type Param is instantiated to A.

The constant a2 has the type B since the parameter s2 has the type B.

It was a normal behavior up to version 3.6.0-dev.20190723. Since then it leads to errors.

@turtleflyer
Copy link
Author

Also, it is worth to note that the compiler made the wrong conclusion in the error message when it reveals { b: any } & A as { b: any; } & { a: any; } & { c?: any; }

@RyanCavanaugh
Copy link
Member

What was the intent in writing

declare function testA<Param extends A>(param: Param & A): Param;

instead of

declare function testA<Param extends A>(param: Param): Param;

?

@turtleflyer
Copy link
Author

This was an example that simplifies the real-life piece of code exposing the bug. This pattern is useful in a generic function where types are not narrowing properly due to design limitation:

type Animal = { type: 'dog'; forelegs: number } | { type: 'duck'; wings: number };

function calculateForelimbs<A extends Animal>(animal: A): A & { forelimbs: number } {
    return { ...animal, forelimbs: animal.type === 'dog' ? animal.forelegs : animal.wings };
    // won't work due to design limitation
}

It could be fixed by adding & Animal to the type of animal argument:

function calculateForelimbsWorkable<A extends Animal>(animal: A & Animal): A & { forelimbs: number } {
    return { ...animal, forelimbs: animal.type === 'dog' ? animal.forelegs : animal.wings };
    // working like a charm
}

function createSuperAnimal(): Animal {
    const legsOrWings = 100500;
    const typeOfAnimal = Math.random() < 0.5 ? 'dog' : 'duck';

    return typeOfAnimal === 'dog'
        ? { type: typeOfAnimal, forelegs: legsOrWings }
        : { type: typeOfAnimal, wings: legsOrWings }
}

const superAnimalWithLimbsCalculated = calculateForelimbsWorkable(createSuperAnimal());

link to playground

If we make the Animal type more complex:

type Animal = ({ type: 'dog'; forelegs: number } | { type: 'duck'; wings: number }) & { domesticated: boolean };

it still works (link to playground).

When we decide to have property domesticated being optional, it turns out the code is broke:

type Animal = ({ type: 'dog'; forelegs: number } | { type: 'duck'; wings: number }) & { domesticated?: boolean };

link to playground

Now, when version 4.3.2 with improved contextual narrowing for generics has been shipped, it has become less actual. Now we can skip & Animal part.

But the bug is a bug. It may affect the legacy code or be a broken part of a bigger problem.

@turtleflyer
Copy link
Author

I found the use case where this pattern would be still useful:

type A = ({ a: any } | { b: any }) & { c?: any };

declare function f<X extends A>(x: X): X

function testA1<Param extends A | undefined>(param: Param): Param {
    return param && f(param)
    // won't work even in typescript 4.3
};

function testA2<Param extends A | undefined>(param: Param & A): Param {
    return param && f(param)
    // working fine
};

declare const s1: A;

const a1 = testA2(s1); // still error

type B = ({ a: any } | { b: any }) & { c: any };

declare const s2: B;

const a2 = testA2(s2); // wrong type A | undefined, must be B

const a3 = testA1(s1); // A, but testA1 would not work on its own

const a4 = testA1(s2); // B, but testA1 would not work on its own

link to playground

It works fine in typescript 3.3.3333: link

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jun 7, 2021
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jun 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

2 participants