Skip to content

Generic function with rest parameter assignability not transitive #32948

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

Closed
AnyhowStep opened this issue Aug 17, 2019 · 9 comments · Fixed by #33020
Closed

Generic function with rest parameter assignability not transitive #32948

AnyhowStep opened this issue Aug 17, 2019 · 9 comments · Fixed by #33020
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 17, 2019

TypeScript Version:

Search Terms:

Code

/**
 * --strictFunctionTypes
 */

export type ExtendedMapper<HandledInputT, OutputT, ArgsT extends any[]> = (
    (name : string, mixed : HandledInputT, ...args : ArgsT) => OutputT
);

//type a = (name: string, mixed: any, args_0: any) => any
type a = ExtendedMapper<any, any, [any]>;
//type b = (name: string, mixed: any, ...args: any[]) => any
type b = ExtendedMapper<any, any, any[]>;
//3.5.1, [email protected]
//Expected: "y"
//Actual  : "y"
//https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
//https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/40404/artifacts?artifactName=tgz&fileId=AD9D22CF70561BEAF5758E061B61D3FF891ADF664E37C532F5881393CE7DC83202&fileName=/typescript-3.7.0-insiders.20190815.tgz
//Expected: "y"
//Actual  : "n" <-- Intentional?
type test = a extends b ? "y" : "n"

type a2 = (name: string, mixed: any, args_0: any) => any
type b2 = (name: string, mixed: any, ...args: any[]) => any
//3.5.1, [email protected]
//Expected: "y"
//Actual  : "y"
//https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
//https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/40404/artifacts?artifactName=tgz&fileId=AD9D22CF70561BEAF5758E061B61D3FF891ADF664E37C532F5881393CE7DC83202&fileName=/typescript-3.7.0-insiders.20190815.tgz
//Expected: "y"
//Actual  : "y"
type test2 = a2 extends b2 ? "y" : "n"

//a extends b2 extends b
//However, a DOES NOT extend b
//It seems assignability is not transitive...

//"y" for all versions
type aExtendsB2 = a extends b2 ? "y" : "n"
//"y" for all versions
type b2ExtendsB = b2 extends b ? "y" : "n"

type TakeExtendedMapperRest<MapperT extends b> = (
    ["Blah", MapperT]
);

//Works in 3.5.1
//Fails for https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
export type ShouldWorkButFails<T extends a> = (
    //Type 'ExtendedMapper<any, any, [any]>' does not satisfy the constraint 'ExtendedMapper<any, any, any[]>'.
    //  Property '0' is missing in type 'any[]' but required in type '[any]'.ts(2344)
    //a -> b not allowed
    TakeExtendedMapperRest<T>
    //                     ~
);

//Works in 3.5.1
//Works in https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
export type Workaround<T extends a> = (
    TakeExtendedMapperRest<
        //OK!
        //Takes advantage of a -> b2 -> b being allowed
        Extract<T, b2>
    >
);

//Works in 3.5.1
//Fails in https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
type WorkaroundTest = TakeExtendedMapperRest<a>;

//Works in 3.5.1
//Works in https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
//type WorkaroundTest2 = ["Blah", ExtendedMapper<any, any, [any]>]
type WorkaroundTest2 = Workaround<a>; //Success

Expected behavior:

Build in #32924 (comment) should behave the same as 3.5.1 and [email protected]

Assignability should be transitive.

a -> b2 -> b should imply a -> b

Actual behavior:

Assignability is not transitive

a -> b2 -> b but not a -> b

Playground Link:
Playground

Related Issues:

#32924 (comment)

#32924 (comment)

#32924 (comment)

I'm not sure which commit introduced this problem.

@weswigham

@weswigham
Copy link
Member

Assignability has never been transitive - see {a?: number } -> {a?: number; b?: number } -> {b?: number}.

The examples I gave in the thread you discovered it is more compelling - two nearly identical types (identical sans alias symbol) that do not yield identical comparison results.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 17, 2019

Guess I'll include the no-alias version for completeness,

/**
 * --strictFunctionTypes
 */

export type ExtendedMapper<HandledInputT, OutputT, ArgsT extends any[]> = (
    //Using the no-alias example from,
    //https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521930291
    {x:(name : string, mixed : HandledInputT, ...args : ArgsT) => OutputT}["x"]
);

//type a = (name: string, mixed: any, args_0: any) => any
type a = ExtendedMapper<any, any, [any]>;
//type b = (name: string, mixed: any, ...args: any[]) => any
type b = ExtendedMapper<any, any, any[]>;
//3.5.1, [email protected]
//Expected: "y"
//Actual  : "y"
//https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
//https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/40404/artifacts?artifactName=tgz&fileId=AD9D22CF70561BEAF5758E061B61D3FF891ADF664E37C532F5881393CE7DC83202&fileName=/typescript-3.7.0-insiders.20190815.tgz
//Expected: "y"
//Actual  : "y" <-- Using the "no-alias" example, it works as intended
type test = a extends b ? "y" : "n"

type a2 = (name: string, mixed: any, args_0: any) => any
type b2 = (name: string, mixed: any, ...args: any[]) => any
//3.5.1, [email protected]
//Expected: "y"
//Actual  : "y"
//https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476
//https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/40404/artifacts?artifactName=tgz&fileId=AD9D22CF70561BEAF5758E061B61D3FF891ADF664E37C532F5881393CE7DC83202&fileName=/typescript-3.7.0-insiders.20190815.tgz
//Expected: "y"
//Actual  : "y"
type test2 = a2 extends b2 ? "y" : "n"

Playground

It's weird to me that the no-alias version behaves as expected.
They both should resolve to the same type.


That partial object assignability not being transitive makes sense. I forgot about that =x
I think it should be transitive in this particular case, though

@jack-williams
Copy link
Collaborator

jack-williams commented Aug 17, 2019

I think this stems from the special rules for any[] as a rest type rest types.

const list: any[] = [];
const tuple: [any] = list;

const fn: (...args: any[]) => void = undefined as any as (...args: [any]) => void;

A list of type any[] is not assignable to a tuple of type [any], while a rest arg of type any[] is assignable to a rest arg of type [any].

The type alias does not track the context in which a type parameter was used so checking uses the first relationship, rather than the second.

@fatcerberus
Copy link

@jack-williams

A list of type any[] is not assignable to a tuple of type [any], while a rest arg of type any[] is assignable to a rest arg of type [any].

You say this is a "special rule" but... isn't that just a natural consequence of contravariance?

@jack-williams
Copy link
Collaborator

jack-williams commented Aug 18, 2019

The assignment to fn requires that rest args of type any[] are assignable to the rest args if type [any].

While the function assignment is allowed, a corresponding assignment of identifiers of those types would not be.

A correction in my wording: the special treatment does not apply specifically for any, but all function assignments with unbounded rest parameters.

This is effectively the same problem as #31698; specifically see this comment here.

@fatcerberus
Copy link

fatcerberus commented Aug 18, 2019

Right, what I was getting at was it was it was the same as how BaseType is not assignable to DerivedType but a function type that accepts a BaseType as a parameter is assignable to one that accepts a DerivedType - i.e. the special treatment is just normal contravariance?

...unless you mean this applies at the time the function is called?

@jack-williams
Copy link
Collaborator

@fatcerberus

The special treatment is that the normal rules of contravariance of function input are ignored in this particular case. Using the normals rules would suggest than assigning a function that expects a more precise arity (exactly one) to a function that expects a less precise arity (0 or more) would be an error.

const echo = (x: string) => console.log(x);
const fn: (...args: string[]) => void = echo; // no error
fn(); // no error, but echo is called without an argument;

However, the checker will not complain here because it assumes that fn will be called at the correct arity echo expects.

@fatcerberus
Copy link

Oh I see, it can lead to unsound calls. That makes sense now. I admit I hadn't considered the effect of the rest parameter on the function's arity (there's a word you don't see often!): I just saw that clearly, you can use a function that accepts a T[] in place of one that accepts a [T] and there was no problem with that. But because it's a rest parameter that also affects arity, it's not necessary sound.

Thanks for being patient with me, I feel pretty dumb now. 🥴

@jack-williams
Copy link
Collaborator

@fatcerberus Any time - good questions and discussion make the tracker better for everyone that uses it.

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.

5 participants