microsoft / TypeScript

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

Couple apparent bugs with variance checking #53210

Open masaeedu opened 1 year ago

masaeedu commented 1 year ago

Bug Report

šŸ”Ž Search Terms

variance, methods, generics

NB: I did not search very exhaustively, but I did minimize the examples, so should be pretty easy to review and close as dupe or "by design" or whatever.

šŸ•— Version & Regression Information

āÆ Playground Link

Variance checking does not interact correctly with extends:

https://www.typescriptlang.org/play?#code/C4TwDgpgBAYgPAewK7CgQQDRWagQgPigF4o4ANKCAD2AgDsATAZ3WPXwAoA3ALijICUxQrgBQogMYI6TVAEM+8OhC4QATlmWq1hEl2FQuk6bKgAjRXDpIAtmfWaV63VDnGZqNRCZIANsD4tdTYzDgAWACYhAHpoqAB3AAsEBDAmIA

Variance checking does not interact correctly with "records with methods" types:

https://www.typescriptlang.org/play?#code/C4TwDgpgBAGgPAewK7CgZQHxQLxQN4BQUUAHgBQDOAXOgJQ0B2EAbhAE4EC+BBAxggwqoAhjXhNWbLLjykaFHFgXd+g1ACMxcJAwDWDBAHcG0qML4ChUQwAsECMNSgT2OKOoB05ACwAmWgRAA

šŸ’» Code

Variance checking does not interact correctly with extends:

type F<out A, out B> = <X extends A = A>(v: X) => B

const a: F<never, never> = v => v
const b: F<unknown, never> = a
const result: never = b(42) // whoops

Variance checking does not interact correctly with "records with methods" types:

type X<out S> = {
  x(s: S): never
}

const a: X<never> = { x: s => s }
const b: X<unknown> = a
const whoops: never = b.x(42)

šŸ™ Actual behavior

In both cases the typechecker allows me to inhabit never.

šŸ™‚ Expected behavior

The typechecker should reject my incorrect variance annotations (F<out A, ...> and X<out S>) and not allow me to inhabit never.


Incidentally this came to light out of some discussion in the "forall for non-functions" issue here: https://github.com/microsoft/TypeScript/issues/17574#issuecomment-1465094919, but these things don't really seem materially related.

Related to #48240.

scorbiclife commented 1 year ago

Came here to report that the out keyword is optional and it also happens here:

type F<out A, out B> = <X>(v: X extends A ? X : never) => B

const a: F<never, never> = v => v
const b: F<number, never> = a
const result: never = b(42) // whoops
masaeedu commented 1 year ago

@nightlyherb Hmm, not sure I'm following. The out keyword in the repros given is material, the point is to demonstrate that the typechecker agrees with the incorrect/unsound variance annotation given.

The snippet you gave also contains an out annotation, is that a typo?

scorbiclife commented 1 year ago

I just tried to say that all the examples you gave and the example I gave exhibits buggy behavior with or without variance annotations.

The buggy behavior without variance annotations might be a separate issue, but I thought it was worth a mention since it's so similar and it might be a related issue.

masaeedu commented 1 year ago

The buggy behavior without variance annotations might be a separate issue, but I thought it was worth a mention since it's so similar and it might be a related issue.

@nightlyherb You're right that the issues are related, in fact I think they're mostly indistinguishable. The buggy behavior we're talking about is the ability to assign const b: ... = a, which is accepted due to the (incorrectly) assumed covariance of a particular parameter. If the typechecker correctly determined that the relevant type parameters were contravariant instead, both my incorrect out annotations and the assignment would be rejected.

RyanCavanaugh commented 1 year ago

A type parameter appearing only in a constraint is kinda sus, and generally you would want to write X & A in a usage position wherever you write <X extends A>(... But that doesn't even work here. I think the variance is just not being measured correctly. Examples:

// Incorrect: Fails to error
type F1<out A> = <X>(v: X & A) => unknown;
// Correctly errors
type F2<out A> = <X>(v1: X, v2: A) => unknown;
// Correctly errors
type F3<out A> = <X>(v2: A) => X;
scorbiclife commented 1 year ago

I have encountered a situation where this sus usage could be useful.

// F seems to behave like a generic type contravariant over Xi,
// but typescript doesn't error on either in Xi or out Xi,
// so I cannot use this generic type.
type F<Xi> = <X extends Xi>(x: X) => X;

// I can instantiate this generic type manually
// to observe the contravariant behavior.
// number is a subtype of unknown and
// F<unknown> seems to be a subtype of F<number>
declare const funknown = <X extends unknown>(x: X) => X;
declare const fnumber = <X extends number>(x: X) => X;
const funknown2: typeof funknown = fnumber; // error
const fnumber2: typeof fnumber = funknown; // no error

// this does seem natural because `funknown` is assignable
// to a superset of types assignable from `fnumber`

May I ask, is my observation correct? i.e. is F contravariant over Xi in current typescript?