镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Generic interface should not be invariant when type arg only used in parameter list of function property  #32674

Description

@AnyhowStep

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:

Activity

  1. jack-williams commented on Aug 6, 2019

    @jack-williams
    Collaborator

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

  2. jcalz commented on Dec 19, 2019

    @jcalz
    Contributor

    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

  3. jack-williams commented on Dec 19, 2019

    @jack-williams
    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.
  4. jcalz commented on Dec 19, 2019

    @jcalz
    Contributor

    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.

  5. jack-williams commented on Dec 19, 2019

    @jack-williams
    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.

  6. ahejlsberg commented on Oct 25, 2020

    @ahejlsberg
    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.

  7. added
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    RescheduledThis issue was previously scheduled to an earlier milestone
    on Oct 25, 2020
  8. ahejlsberg commented on Oct 25, 2020

    @ahejlsberg
    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.

  9. AnyhowStep commented on Nov 13, 2020

    @AnyhowStep
    ContributorAuthor

    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

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

      Development

      No branches or pull requests

      Issue actions