microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.96k stars 12.48k forks source link

`never` assignability check disappears for method call from generic function #57453

Closed rotu closed 8 months ago

rotu commented 8 months ago

🔎 Search Terms

never bivariant method assignable generic

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/dev/bug-workbench/?ts=5.4.0-dev.20240220#code/C4TwDgpgBAYg9nAPAFQHxQLxQN4CgoFQCGAshMABZwAmAFMAFzICUDAbnAJbUA0+hAegFQAzsABORAHYBzCABsQPUZwC2YRVACuUgMZxVqiFOCdZUStCkQAHsCjyz0XUS0iIIixWgAjBXAB3KFtIXWAIamDxcThxCzgocQgiMEgicQBCfgIhYhgdMM44KQZ6JmYMVA5uXABfXFxqCF15dOdisSg4HwArBngkazYIcVRcbp6AOlJyKjoARmYoXIBBcRktIxMugDMLcGgAcilNv3FDqE5PKTh7IhERThkpIh95aGAEsHSiI3C4uB7UCQKDHCDDc64XJNSBSaieYpQfSGYzhSKOaxQ4QTab5PSmYq0RYNfRSToueTyMiUGiYKAoHioWhEfoIFCoZQ+cqVPCEYjTalzWg+Zh1ElESmCmi0CbKRbLYQhZpoqIxcS4IA

💻 Code

type Foo<T> = {
    aMethod(t:T):void,
    // strangely, simply uncommenting the next line causes the below expected error to reappear!
    // aFunction:(t:T)=>void
}

declare const obj:Foo<never>
obj.aMethod(1) // Argument of type 'number' is not assignable to parameter of type 'never'
// depends on commented line
// obj.aFunction(1)

const callMethod = <T,>(a:Foo<T>, b:T)=>{
    a.aMethod(b)
}

callMethod(obj, 1) // expected error

🙁 Actual behavior

callMethod does not issue a type warning that "Argument of type 'number' is not assignable to parameter of type 'never'".

Doing the same call outside a generic function body causes the error again.

🙂 Expected behavior

I expect a warning, similar to if I had called the method directly or if the method were instead a function.

Additional information about the issue

Note this was originally discovered in the context of Array<never> which, when used in a generic function, allows one to unsafely push any type T: https://github.com/microsoft/TypeScript/issues/57419#issuecomment-1949133085.

RyanCavanaugh commented 8 months ago

This is just method parameter bivariance.

// Neither are errors
const a: Foo<never> = null! as Foo<1>;
const b: Foo<1> = null! as Foo<never>;
rotu commented 8 months ago

This is just method parameter bivariance.

// Neither are errors
const a: Foo<never> = null! as Foo<1>;
const b: Foo<1> = null! as Foo<never>;

Blech! I was so focused on the variance of aMethod that missed the variance of callMethod's parameters. You're right. callMethod gets instantiated with type 1 instead of never and obj get implicitly coerced from Foo<never> to Foo<1> in the function call.

I can see that this is fixed by instead annotating it: const callMethod = <T,>(a:Foo<T>, b:NoInfer<T>).