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 callback parameter defined with conditional tuple type #26013

Closed
bterlson opened this issue Jul 27, 2018 · 8 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@bterlson
Copy link
Member

bterlson commented Jul 27, 2018

TypeScript Version: 3.1.0-dev.20180727

Code

// ConditionalTuple could also return just [] in the false branch, issue still repros
type ConditionalTuple<T> = [T] extends [undefined] ? [] : [T];

// works fine!
declare var f1: (...args: any[]) => void;
declare var f2: <T>(...args: ConditionalTuple<T>) => void;
declare var f3: (...args: ConditionalTuple<number>) => void;
f2 = f1;
f3 = f1;

// but if we move the signatures above into a callback parameter with that signature,
// assignability breaks - the types of cb are incompatible.
declare var cb1: (cb: (...args: any[]) => void) => void;
declare var cb2: <T>(cb: (...args: ConditionalTuple<T>) => void) => void;
declare var cb3: (cb: (...args: ConditionalTuple<number>) => void) => void;

cb2 = cb1; // can't assign to cb2
cb3 = cb1; // this works though

Expected behavior:
I am able to construct callback signatures with generic conditional types and tuples that are assignable with the maximally general callback signature (cb1).

Actual behavior:
cb1 cannot be assigned to cb2, but removing the generic parameter ala cb3 makes assignability work as expected.

@mattmccutchen
Copy link
Contributor

For cb1 to be assignable to cb2, the callback of cb2 needs to be assignable to the callback of cb1 (contravariance). This isn't working because the following code in src/compiler/checker.ts compareSignaturesRelated around line 10564 is triggering, because the source rest type ConditionalType<T> is generic and the target rest type any[] is not:

            const sourceCount = getParameterCount(source);
            const sourceGenericRestType = getGenericRestType(source);
            const targetGenericRestType = sourceGenericRestType ? getGenericRestType(target) : undefined;
            if (sourceGenericRestType && !(targetGenericRestType && sourceCount === targetCount)) {
                return Ternary.False;
            }

I'm unsure what this code is supposed to be doing.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 27, 2018

@ahejlsberg was working on a fix for assigning a type to a conditional type if it is assignable to the two branches.

@mhegazy mhegazy added the Bug A bug in TypeScript label Jul 27, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 27, 2018
@mattmccutchen
Copy link
Contributor

@ahejlsberg was working on a fix for assigning a type to a conditional type if it is assignable to the two branches.

Unless I'm missing something, the code is bailing out at the point I quoted before it even tests assignability to the conditional type, so something additional will need to be done.

@ahejlsberg
Copy link
Member

The conditional type is irrelevant here. The examples can be simplified to just:

declare var f1: (...args: any[]) => void;
declare var f2: <T extends any[]>(...args: T) => void;
f1 = f2;  // Ok
f2 = f1;  // Ok

declare var cb1: (cb: (...args: any[]) => void) => void;
declare var cb2: <T extends any[]>(cb: (...args: T) => void) => void;
cb1 = cb2;  // Ok
cb2 = cb1;  // Error

In the second example, the args parameter is co-variant, i.e. it is in an output position (a parameter in a callback parameter is effectively checked in the same manner as a return type). So, we're dealing with something similar to:

declare var ff1: () => any[];
declare var ff2: <T extends any[]>() => T;
ff1 = ff2;  // Ok
ff2 = ff1;  // Error

The second assignment fails because any[] is not assignable to T which seems entirely reasonable. So, I don't think there is a bug here.

@mattmccutchen The code you're looking at is indeed the place we error. It basically says that a generic rest parameter in the source must be matched by a generic rest parameter in the target, because only another generic type could be assignable here (i.e. for a source T extends any[], only a related U extends T would ever be assignable to T).

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript labels Jul 31, 2018
@ahejlsberg ahejlsberg removed this from the TypeScript 3.1 milestone Jul 31, 2018
@mattmccutchen
Copy link
Contributor

The second assignment fails because any[] is not assignable to T which seems entirely reasonable.

This is inconsistent with the behavior when the rest parameter is a non-generic tuple type. For example:

declare var cb1: (cb: (...args: any[]) => void) => void;
declare var cb2: (cb: (...args: [any]) => void) => void;
cb1 = cb2;  // Ok
cb2 = cb1;  // Ok

declare var ff1: () => any[];
declare var ff2: () => [any];
ff1 = ff2;  // Ok
ff2 = ff1;  // Error

So I think there is a bug here: either cb2 = cb1 should be an error in this example, or it should be allowed in the previous example.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jul 31, 2018

@mattmccutchen that's because that effectively gets flattened to

declare var cb1: (cb: (...args: any[]) => void) => void;
declare var cb2: (cb: (arg0: any) => void) => void;

cb1 = cb2;
cb2 = cb1;

and

When comparing call or construct signatures, parameter names are ignored and rest parameters correspond to an unbounded expansion of optional parameters of the rest parameter element type.

Which is definitely an inconsistency in how tuple types and parameter lists are compared, but it seems unrelated here.


The thing is that you really don't know whether T will be instantiated with some subtype of Array<any> (e.g. imagine a MySuperCoolArray<T> which has an extra member). I do think it's reasonable to say any[] is not assignable to a T extends any[], but I'm not sure how to otherwise write the code @bterlson is aiming for.

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Jul 31, 2018

The thing is that you really don't know whether T will be instantiated with some subtype of Array<any> (e.g. imagine a MySuperCoolArray<T> which has an extra member).

TypeScript doesn't normally allow that:

interface MySuperCoolArray<E> extends Array<E> { 
    hello: number;
}

// Error: A rest parameter must be of an array type.
declare var cb2: (cb: (...args: MySuperCoolArray<any>) => void) => void;

So in principle, when we see:

declare var cb2: <T extends any[]>(cb: (...args: T) => void) => void;

we should have a constraint that T be an array or tuple type. TypeScript has no way to express such a constraint, but IMO the least bad thing we can do is allow an any[] rest parameter to be assigned to a T rest parameter, like we allow it to be assigned to any specific type that is a valid type for a rest parameter.

As an aside, this isn't an error on the playground and it probably should be:

interface MySuperCoolArray<E> extends Array<E> { 
    hello: number;
}

declare var cb2: <T extends MySuperCoolArray<any>>(cb: (...args: T) => void) => void;

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants