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 interface should not be invariant when type arg only used in parameter list of function property #32674

Closed
AnyhowStep opened this issue Aug 2, 2019 · 8 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@AnyhowStep
Copy link
Contributor

TypeScript Version: 3.5.1

Search Terms:

generic interface, invariant, contravariant, type arg, parameter list, function property

Code

//strictFunctionTypes enabled
declare let _a : {
    //This is a function property, not a method
    _ : (arg : ["a", "b", bigint]) => void
};
declare let _b : {
    //This is a function property, not a method
    _ : (arg : ["a", "b", bigint|null]) => void
};
/**
 * Assignment OK!
 */
_a = _b;

/**
 * Assignment not allowed, as expected
 * 
 * Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
 */
_b = _a;

////////////////////////////////////

type Transform<T extends { [tableAlias:string] : { [columnAlias:string] : any } }> = (
    {
        [tableAlias in keyof T] : (
            {
                [columnAlias in keyof T[tableAlias]] : (
                    [tableAlias, columnAlias, T[tableAlias][columnAlias]]
                )
            }[keyof T[tableAlias]]
        )
    }[keyof T]
);

interface X<T extends { [tableAlias:string] : { [columnAlias:string] : any } }> {
    //This is a function property, not a method
    _ : (arg : Transform<T>) => void
}

declare let ax : X<{
    a : {
        b : bigint,
    }
}>;
/**
 * ax._ : (arg: ["a", "b", bigint]) => void
 * _a._ : (arg: ["a", "b", bigint]) => void
 */
ax._

declare let bx : X<{
    a : {
        b : bigint|null,
    }
}>;
/**
 * bx._ : (arg: ["a", "b", bigint | null]) => void
 * _b._ : (arg: ["a", "b", bigint | null]) => void
 */
bx._

/**
 * Expected : Assignment OK!
 * Actual   : Assignment not allowed
 * 
 * Type '{ b: bigint | null; }' is not assignable to type '{ b: bigint; }'
 */
ax = bx;

/**
 * Assignment not allowed, as expected
 * 
 * Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
 */
bx = ax;

//Assignment OK!
_a = ax;
//Assignment OK!
_a = bx;

/**
 * Assignment not allowed, as expected
 * 
 * Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
 */
_b = ax;
//Assignment OK!
_b = bx;

//Assignment OK!
ax = _a;
//Assignment OK!
ax = _b;

/**
 * Assignment not allowed, as expected
 * 
 * Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
 */
bx = _a;
//Assignment OK!
bx = _b;

Expected behavior:

This should be allowed,

ax = bx;

Actual behavior:

/**
 * Expected : Assignment OK!
 * Actual   : Assignment not allowed
 * 
 * Type '{ b: bigint | null; }' is not assignable to type '{ b: bigint; }'
 */
ax = bx;

Playground Link: Playground

Related Issues:

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Aug 5, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Aug 5, 2019
@jack-williams
Copy link
Collaborator

I think this might be a duplicate / related to #32311.

@jcalz
Copy link
Contributor

jcalz commented Dec 19, 2019

Is this the same or a separate issue:

interface Fn<A, B> {
    (a: A): B;
    done: (fn: (b: B) => void) => Fn<A, void>;
}

declare const f: Fn<string, number>;
const g: Fn<"a", number> = f; // error!?
// Type 'Fn<string, number>' is not assignable to type 'Fn<"a", number>'.
// Type 'string' is not assignable to type '"a"'.(2322) 
// ... but why is Fn<A, B> not contravariant in A?

from here

@jack-williams
Copy link
Collaborator

I think that might be separate because the issue you post is probably related to recursive types. My guess as to what is going on there is:

  1. Measuring variance for Fn<A, B> observes A as contravariant from the function input.
  2. The recursive instantiation Fn<A, void> has a different type id to Fn<A, B> so it doesn't get related using the recursive assumption. Relating Fn<A, void> to Fn<A, void> (where A is a marker) probable hits the following code path:
// When variance information isn't available we default to covariance. This happens
// in the process of computing variance information for recursive types and when
// comparing 'this' type arguments.
  1. The effect of 2 is to mark A as covariant, and in conjuction with 1, marks A as invariant.

@jcalz
Copy link
Contributor

jcalz commented Dec 19, 2019

Thanks. I wonder if this warrants a brand new issue or should be added as a curiosity onto #1394 for a case where an explicit contravariance annotation would be useful.

@jack-williams
Copy link
Collaborator

I think this warrants a new issue, at least for future referencing. The core problem is that Fn<A, B> gets marked with a special marker flag (to ensure structural checking) but Fn<A, void> does not.

The closest duplicate is probably #33872, but that was a specific regression.

@ahejlsberg
Copy link
Member

Here's a simpler repro:

// Covariant because T[keyof T] has no alias symbol and we always relate structurally

type A<T> = T[keyof T];

declare let a1: A<{ a: bigint }>;
declare let a2: A<{ a: bigint | null }>;
a1 = a2;  // Error
a2 = a1;

// Invariant because of variance measurement for B<T>

type B<T> = { prop: T[keyof T] };

declare let b1: B<{ a: bigint }>;
declare let b2: B<{ a: bigint | null }>;
b1 = b2;  // Error
b2 = b1;  // Error

We consider T[keyof T] to be invariant in T. Ideally we should consider it contravariant, but as outlined in #32311 there are reasons we can't. So, variance measurements for T end up being too conservative, and T[keyof T] becomes a device for revealing when we perform variance measurements. In the example above, it reveals that we don't measure variance for A<T> because we can't attach a type alias to an indexed access type. However, we do measure variance for B<T> and thus end up being more conservative.

It's a tough problem to solve. If type relations were fully evaluated structurally at all times we'd get the expected results, but we'd also have horrible performance (we've tried, it's not pretty). So, not sure there's much we can do here.

@ahejlsberg ahejlsberg added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone labels Oct 25, 2020
@ahejlsberg ahejlsberg removed this from the TypeScript 4.1.0 milestone Oct 25, 2020
@ahejlsberg ahejlsberg removed their assignment Oct 25, 2020
@ahejlsberg
Copy link
Member

Actually, let me clarify a bit. T[keyof T] is sometimes covariant, sometimes contravariant, and sometimes invariant, depending on how T varies. For example

  • for a supertype { a: string | number } and a subtype { a: string }, T[keyof T] is covariant, but
  • for a supertype { a: string } and a subtype { a: string, b: boolean }, T[keyof T] is contravariant, and
  • for a supertype { a: string | number } and a subtype { a: string, b: boolean }, T[keyof T] is invariant.

So, the only safe thing to assume is that T[keyof T] is invariant, but it may indeed be too conservative at times.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Nov 13, 2020

If we could manually annotate type params with a particular variance in the future, would that solve this problem?

Or would we run into cases where it would be too expensive to check that the annotated variance is correct?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

5 participants