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 https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521819476 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:
https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521826091
https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521834620
https://github.com/microsoft/TypeScript/pull/32924#issuecomment-521930291
I'm not sure which commit introduced this problem.
@weswigham
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.
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"
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
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.
@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?
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.
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?
@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.
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. 馃ゴ
@fatcerberus Any time - good questions and discussion make the tracker better for everyone that uses it.
Most helpful comment
@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.
However, the checker will not complain here because it assumes that
fnwill be called at the correct arityechoexpects.