Skip to content

Check order dependence with mutually-recursive non-unary generics #44572

Closed
@erikbrinkman

Description

@erikbrinkman
Contributor

Bug Report

Adding an implementation of an interface allows an invalid assignability check of the interface to pass

🔎 Search Terms

assignability, unnecessary implementation

🕗 Version & Regression Information

  • This changed between versions 3.7.5 and 3.8.3

It's still present in all versions including nightly.

⏯ Playground Link

Playground link with relevant code

Thanks @MartinJohns for the link

💻 Code

interface Parent<A, B> {
  getChild(): Child<A, B>;

  iter(): Iterable<Parent<A, B>>;
}

interface Child<A, B>
  extends Parent<A, B> {
  readonly a: A;
  readonly b: B;
}

class Impl<A, B> implements Parent<A, B> {
  constructor(readonly child: Child<A, B>) {
  }

  getChild(): Child<A, B> {
    return this.child;
  }

  *iter(): Iterable<Parent<A, B>> {
    const map = new Map<Child<unknown, unknown>, Child<A, B>[]>();

    function* gen(
      inp: Child<A, B>
    ): Iterable<Child<A, B>> {
      yield* map.get(inp) || [];
    }
  }
}

const x: Parent<unknown, unknown> = {} as any;
const _: Parent<null, unknown> = x; // should not pass

The final assignment should not pass. It accurately errors in 3.7.5, if the Impl class is removed (or any aspect of the iter implementation is modified), or by adding a sentinel type like sentinel?: A to the Parent interface to aid in type checking.

🙁 Actual behavior

No error is thrown in the final assignment.

🙂 Expected behavior

A n error is throw:

Type 'Parent<unknown, unknown>' is not assignable to type 'Parent<null, unknown>'.
  Type 'unknown' is not assignable to type 'null'.

Activity

MartinJohns

MartinJohns commented on Jun 13, 2021

@MartinJohns
Contributor

Unfortunately, this only occurs across several files, so I can't link to the playground.

It's reproducible on the playground: Playground link

Deleting the unused Impl class results in the error being shown.

changed the title [-]Typescript passing incorrect assignability check in very strange circumstance.[/-] [+]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/+] on Jun 13, 2021
changed the title [-]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/-] [+]Check order dependence with mutually-recursive non-unary generics[/+] on Jun 14, 2021
RyanCavanaugh

RyanCavanaugh commented on Jun 14, 2021

@RyanCavanaugh
Member

Simplified and cleaned up a little to make the violation more apparent

interface Parent<A> {
  child: Child<A> | null;
  parent: Parent<A> | null;
}

interface Child<A, B = unknown> extends Parent<A> {
  readonly a: A;
  // This field isn't necessary to the repro, but the
  // type parameter is, so including it
  readonly b: B;
}

function fn<A>(inp: Child<A>) {
  // This assignability check defeats the later one
  const a: Child<unknown> = inp;
}

// Allowed initialization of pu
const pu: Parent<unknown> = { child: { a: 0, b: 0, child: null, parent: null }, parent: null };
// Should error
const notString: Parent<string> = pu;
// Unsound read on .child.a
const m: string = notString.child!.a;
andrewbranch

andrewbranch commented on Jul 19, 2021

@andrewbranch
Member

Debugging notes: variance measurement for Parent is getting set to VarianceFlags.Independent, implying that its type parameter is never witnessed at all. It arrived at this conclusion by checking the assignability of Parent instantiated with marker types. It first checks assignability in both directions with instantiations with super/sub-related marker types, and
assignability appears to return true in both directions; however, it actually is returning Ternary.Unknown, due to being unable to answer questions about the assignability of the types' parent and child properties without knowing their variances. After (incorrectly) concluding that Parent is bivariant on A, it checks another set of instantiations with markers that are unrelated to each other. That too comes back as Ternary.Unknown but is interpreted as true, so the variance gets updated to Independent, since instantiating Parent with all kinds of different markers with different assignability to each other apparently had no effect on the instantiations' assignability to each other.

I'm not sure if any of those comparisons ever actually looked at a and b, which should provide some non-recursive concrete variance information. I'm also not sure if outofbandVarianceMarkerHandler should have been called at some point, but it was not.

added
RescheduledThis issue was previously scheduled to an earlier milestone
on Aug 20, 2021
andrewbranch

andrewbranch commented on Aug 20, 2021

@andrewbranch
Member

@weswigham, I may need some help/advice on this one.

weswigham

weswigham commented on Aug 20, 2021

@weswigham
Member

We should probably make a Ternary.Unknown result witnessed by getVarianceWorker result in a Unmeasurable variance.

21 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @erikbrinkman@weswigham@andrewbranch@ahejlsberg@MartinJohns

      Issue actions

        Check order dependence with mutually-recursive non-unary generics · Issue #44572 · microsoft/TypeScript