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

Assignability of covariant types with bivariant methods now fails in certain intersection types #51620

Closed
jcalz opened this issue Nov 21, 2022 · 1 comment Β· Fixed by #56218
Closed
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@jcalz
Copy link
Contributor

jcalz commented Nov 21, 2022

Bug Report

πŸ”Ž Search Terms

intersection, contravariant, bivariant, Array, Set, union, covariant, assignability

πŸ•— Version & Regression Information

  • This changed between versions 4.9.0-dev.20221013 and 4.9.0-dev.20221014

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

let x2: Set<(data: string) => void> & Set<(data: number) => void>
x2 = new Set<(data: string | number) => void>(); // okay 4.9.0-dev.20221013-, error 4.9.0-dev.20221014+

πŸ™ Actual behavior

The assignment fails in TS4.9+ with Type 'Set<(data: string | number) => void>' is not assignable to type 'Set<(data: string) => void> & Set<(data: number) => void>'. and Types of property 'add' are incompatible.

πŸ™‚ Expected behavior

The assignment should presumably succeed as it did in TS 4.8-.

  • Given that Set<T> is considered to be covariant in T (despite being "morally" invariant due to methods like add()),
  • and given that (x: T) => void is considered to be contravariant in T,
  • then Set<(x: T)=>void> & Set<(x: U)=>void> should (probably?) be assignable to Set<(x: T | U) => void>.

A Stack Overflow question has run into this.

It looks like #51140 might be responsible for the change.

This also occurs with Array<T> or any type considered covariant but having bivariant methods; if you remove the bivariant methods from Set then the assignment succeeds.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Dec 1, 2022
@ahejlsberg
Copy link
Member

This one is interesting. The immediate cause of the change in behavior is #51140. However, that PR simply improved our checking enough to reveal a deeper issue that has existed ever since #15104 was introduced in TS 2.4. In that PR we started checking callback parameters covariantly instead of bivariantly:

type Covar<T> = { setCallback(cb: (value: T) => void): void }

declare let cu: Covar<unknown>;
declare let cs: Covar<string>;
cu = cs;  // Ok
cs = cu;  // Error, but wasn't before #15104

The specific change in #15104 was to detect parameters of single-signature function types and exclude them from the bidirectional checking we otherwise do for parameters of methods. But there's a subtle inconsistency that can arise from this:

type Bivar<T> = { set(value: T): void };

declare let bu: Bivar<unknown>;
declare let bs: Bivar<string>;
bu = bs;  // Ok
bs = bu;  // Ok

declare let bfu: Bivar<(x: unknown) => void>;
declare let bfs: Bivar<(x: string) => void>;
bfu = bfs;  // Ok
bfs = bfu;  // Ok

Above, Bivar<T> is bivariant in T and we measure it's variance accordingly. That means that two instantiations of Bivar<T> are related as long as the type arguments for T are related in one direction or the other. But, in the second example above, we also have a rule that says methods with parameters of single-signature function types should relate covariantly, and the set method qualifies as such in an instantiation of Bivar<T> where T is a single-signature function type. Above, bivariant checking wins because we trust the measured variance for T, but when relating structurally we get a different outcome:

type Bivar1<T> = { set(value: T): void };
type Bivar2<T> = { set(value: T): void };

declare let b1fu: Bivar1<(x: unknown) => void>;
declare let b2fs: Bivar2<(x: string) => void>;
b1fu = b2fs;  // Ok
b2fs = b1fu;  // Error

That's a problem. And the root cause of this issue. Here's the effect with a Set-like type:

type SetLike<T> = { set(value: T): void, get(): T }

declare let sx: SetLike1<(x: unknown) => void>;
declare let sy: SetLike1<(x: string) => void>;
sx = sy;  // Error
sy = sx;  // Ok

type SetLike1<T> = { set(value: T): void, get(): T }
type SetLike2<T> = { set(value: T): void, get(): T }

declare let s1: SetLike1<(x: unknown) => void>;
declare let s2: SetLike2<(x: string) => void>;
s1 = s2;  // Error
s2 = s1;  // Error, but shouldn't be

To fix this we need to slightly amend the callback checking rules such that we only special case parameters of single-signature function types when they have single-signature function types for all possible instantiations of the containing signature. In other words, no special callback checking when the pattern arises due to instantiation of a parameter with a naked generic type.

@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Oct 25, 2023
@ahejlsberg ahejlsberg added the Fix Available A PR has been opened for this issue label Oct 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants