-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Functions with same intersection and conditional type in parameter list not assignable to each other #32442
Comments
If you remove the /**
* We should never be able to create a value of this type legitimately.
*
* `ErrorMessageT` is our error message
*/
interface CompileError<ErrorMessageT extends any[]> {
/**
* There should never be a value of this type
*/
readonly __compileError : never;
}
type ErrorA = CompileError<["I am error A"]>;
type ErrorB = CompileError<["I am error B"]>;
declare const errorA : ErrorA;
/**
* Different compile errors are assignable to each other.
*/
const errorB : ErrorB = errorA;
/**
* Pretend this is `v1.0.0` of your library.
*/
declare function foo <N extends number> (
/**
* This is how we use `CompileError<>` to prevent `3` from being
* a parameter
*/
n : (
(Extract<3, N> extends never ?
N :
CompileError<[3, "is not allowed; received", N]>)
)
) : void;
/**
* Argument of type '3' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", 3]>'.
*/
foo(3);
/**
* OK!
*/
foo(5);
/**
* Argument of type '3 | 5' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", 3 | 5]>'.
*/
foo(5 as 3|5);
/**
* Argument of type 'number' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", number]>'.
*/
foo(5 as number);
///////////////////////////////////////////////////////////////////
/**
* The same as `foo<>()` but with a different error message.
*
* Pretend this is `v1.1.0` of your library.
*/
declare function bar <N extends number> (
n : (
(Extract<3, N> extends never ?
N :
CompileError<[3, "is not allowed; received", N]>)
)
) : void;
/**
* Expected: Assignable to each other
* Actual: Assignable to each other
*/
const fooIsAssignableToBar : typeof bar = foo;
const barIsAssignableToFoo : typeof foo = bar; |
The motivation behind introducing intersection types is so that multiple compile-time requirements can be chained together, declare function bar <N extends number> (
n : (
N &
(Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>) &
(Extract<4, N> extends never ? unknown : CompileError<[4, "is not allowed; received", N]>) &
(SomeOtherComplicatedCondition<N>)
)
) : void; Of course, with such a "basic" type like A code snippet of something more complicated, export type AssertNotInPreviousJoinsImpl<
QueryT extends IQuery,
AliasedTableT extends IAliasedTable
> = (
QueryT["_joins"] extends IJoin[] ?
(
Extract<
AliasedTableT["alias"],
JoinArrayUtil.TableAliases<QueryT["_joins"]>
> extends never ?
unknown :
CompileError<[
"Alias",
Extract<
AliasedTableT["alias"],
JoinArrayUtil.TableAliases<QueryT["_joins"]>
>,
"already used in previous JOINs",
JoinArrayUtil.TableAliases<QueryT["_joins"]>
]>
) :
unknown
);
export type AssertNotInParentJoinsImpl<
QueryT extends IQuery,
AliasedTableT extends IAliasedTable
> = (
QueryT["_parentJoins"] extends IJoin[] ?
(
Extract<
AliasedTableT["alias"],
JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
> extends never ?
unknown :
CompileError<[
"Alias",
Extract<
AliasedTableT["alias"],
JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
>,
"already used in parent JOINs",
JoinArrayUtil.TableAliases<QueryT["_parentJoins"]>
]>
) :
unknown
);
export type AssertNoUsedRefImpl<
AliasedTableT extends IAliasedTable
> = (
Extract<keyof AliasedTableT["usedRef"], string> extends never ?
unknown :
CompileError<[
"Derived table",
AliasedTableT["alias"],
"must not reference outer query tables",
Writable<
ColumnIdentifierUtil.FromColumnRef<AliasedTableT["usedRef"]>
>
]>
);
export type AssertValidJoinTargetImpl<
QueryT extends IQuery,
AliasedTableT extends IAliasedTable
> = (
& AssertNotInPreviousJoinsImpl<QueryT, AliasedTableT>
& AssertNotInParentJoinsImpl<QueryT, AliasedTableT>
& AssertNoUsedRefImpl<AliasedTableT>
);
export type AssertValidJoinTarget<
QueryT extends IQuery,
AliasedTableT extends IAliasedTable
> = (
AliasedTableT &
AssertValidJoinTargetImpl<QueryT, AliasedTableT>
); If all the conditions are satisfied, If at least one of the conditions fail, it resolves to |
It seems like if I nest conditional types instead of introducing intersections, it works, /**
* We should never be able to create a value of this type legitimately.
*
* `ErrorMessageT` is our error message
*/
interface CompileError<ErrorMessageT extends any[]> {
/**
* There should never be a value of this type
*/
readonly __compileError : never;
}
type ErrorA = CompileError<["I am error A"]>;
type ErrorB = CompileError<["I am error B"]>;
declare const errorA : ErrorA;
/**
* Different compile errors are assignable to each other.
*/
const errorB : ErrorB = errorA;
type IfExtractExtendsNever<A, B, TrueT, FalseT> = (
Extract<A, B> extends never ? TrueT : FalseT
);
type Condition0<N, TrueT> = (
IfExtractExtendsNever<
3, N,
TrueT,
CompileError<[3, "is not allowed; received", N]>
>
);
type Condition1<N, TrueT> = (
IfExtractExtendsNever<
4, N,
TrueT,
CompileError<[4, "is not allowed; received", N]>
>
);
type Condition2<N, TrueT> = (
IfExtractExtendsNever<
3, N,
TrueT,
CompileError<[3, "is not allowed; received", N, "Same as Condition0 but with different error message"]>
>
);
type Condition3<N, TrueT> = (
IfExtractExtendsNever<
4, N,
TrueT,
CompileError<[4, "is not allowed; received", N, "Same as Condition1 but with different error message"]>
>
);
/**
* Pretend this is `v1.0.0` of your library.
*/
declare function foo <N extends number> (
/**
* This is how we use `CompileError<>` to prevent `3` from being
* a parameter
*/
n : (
Condition0<N, Condition1<N, N>>
)
) : void;
/**
* Argument of type '3' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", 3]>'.
*/
foo(3);
/**
* OK!
*/
foo(5);
/**
* Argument of type '3 | 5' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", 3 | 5]>'.
*/
foo(5 as 3|5);
/**
* Argument of type 'number' is not assignable to parameter of type
* 'CompileError<[3, "is not allowed; received", number]>'.
*/
foo(5 as number);
///////////////////////////////////////////////////////////////////
/**
* The same as `foo<>()` but with a different error message.
*
* Pretend this is `v1.1.0` of your library.
*/
declare function bar <N extends number> (
n : (
Condition2<N, Condition3<N, N>>
)
) : void;
/**
* Expected: Assignable to each other
* Actual: Assignable to each other
*/
const fooIsAssignableToBar : typeof bar = foo;
const barIsAssignableToFoo : typeof foo = bar; |
This assignability situation is actually making life kind of hard ._. /**
* We should never be able to create a value of this type legitimately.
*
* `ErrorMessageT` is our error message
*/
interface CompileError<ErrorMessageT extends any[]> {
/**
* There should never be a value of this type
*/
readonly __compileError : never;
}
type ErrorA = CompileError<["I am error A"]>;
type ErrorB = CompileError<["I am error B"]>;
declare const errorA : ErrorA;
/**
* Different compile errors are assignable to each other.
*/
const errorB : ErrorB = errorA;
/**
* Expected:
* OK!
*
* Actual:
* Type
* '<N extends number>(n: N & (Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>)) => void'
* is not assignable to type
* '<N extends number>(n: N & (Extract<3, N> extends never ? unknown : CompileError<[3, "is not allowed; received", N]>)) => void'.
* Two different types with this name exist, but they are unrelated.
*/
const blah : (
<N extends number> (
n : (
N &
(Extract<3, N> extends never ?
unknown :
CompileError<[3, "is not allowed; received", N]>)
)
) => void
) = (
<N extends number> (
n : (
N &
(Extract<3, N> extends never ?
unknown :
CompileError<[3, "is not allowed; received", N]>)
)
) : void => {
}
) In the above, you see something like My actual use-case has to do with having function properties (not methods) on a generic class. And the function uses the |
Smaller repro, /**
* Expected:
* OK!
*
* Actual:
* Type
* '<N extends number>(n: N & (3 extends N ? any : any)) => void'
* is not assignable to type
* '<N extends number>(n: N & (3 extends N ? any : any)) => void'.
* Two different types with this name exist, but they are unrelated.
*/
const blah : (
<N extends number> (
n : (
N &
(3 extends N ? any : any)
)
) => void
) = (
<N extends number> (
n : (
N &
(3 extends N ? any : any)
)
) : void => {
}
) |
type BLAH<N> = 3 extends N ? any : any;
/**
* Expected:
* OK!
*
* Actual:
* Types of parameters 'n' and 'n' are incompatible.
* Type 'N & BLAH<N>' is not assignable to type 'N & BLAH<N> & BLAH<N & BLAH<N>>'.
* Type 'N & BLAH<N>' is not assignable to type 'BLAH<N & BLAH<N>>'.
*/
const blah : (
<N extends number> (
n : (
N &
BLAH<N>
)
) => void
) = (
<N extends number> (
n : (
N &
BLAH<N>
)
) : void => {
}
) Even a type assertion does not work, type BLAH<N> = 3 extends N ? any : any;
/**
* Expected:
* OK!
*
* Actual:
* Types of parameters 'n' and 'n' are incompatible.
* Type 'N & BLAH<N>' is not assignable to type 'N & BLAH<N> & BLAH<N & BLAH<N>>'.
* Type 'N & BLAH<N>' is not assignable to type 'BLAH<N & BLAH<N>>'.
*/
const blah : (
<N extends number> (
n : (
N &
BLAH<N>
)
) => void
) = (
<N extends number> (
n : (
N &
BLAH<N>
)
) : void => {
}
) as (
<N extends number> (
n : (
N &
BLAH<N>
)
) => void
) |
So, if I put the generic type param on the //Error, cannot assign
const blah : (
<N extends number> (
n : (
N &
(any extends N ? any : any)
)
) => void
) = (
<N extends number> (
n : (
N &
(any extends N ? any : any)
)
) : void => {
}
) If I put it on the //OK! Can assign
const blah : (
<N extends number> (
n : (
N &
(N extends 3 ? unknown : ["blah"])
)
) => void
) = (
<N extends number> (
n : (
N &
(N extends 3 ? unknown : ["blah"])
)
) : void => {
}
) And I have zero intuition for why this is the case. |
@AnyhowStep TL;DR ? |
Umm... Kind of hard to make a TL;DR. TL;DRI have a generic function. Because of the conditional and intersection type, a different function with the exact same signature is treated as a different type... Even though they are the same type. A type assertion does not work, either. I don't know if it is a bug, design limitation, or by design. And I don't have an intuition for why it thinks it is assignable in some cases, but not assginable in other cases. //Error, cannot assign
const blah : (
<N extends number> (
n : (
N &
(any extends N ? any : any)
)
) => void
) = (
<N extends number> (
n : (
N &
(any extends N ? any : any)
)
) : void => {
}
) Playground showing type assertion not working Notice that the type annotation and the type of the function have the same "type". But the compiler thinks they're different types. But I have found that this is not always the case. In certain cases, the compiler knows they're the same type. Like when I put //OK! Can assign
const blah : (
<N extends number> (
n : (
N &
(N extends 3 ? unknown : ["blah"])
)
) => void
) = (
<N extends number> (
n : (
N &
(N extends 3 ? unknown : ["blah"])
)
) : void => {
}
) |
TypeScript Version: 3.5.1
Search Terms:
function, intersection, conditional type, parameter list, assignable
Code
Expected behavior:
The following should have no errors,
Actual behavior:
It has errors
Playground Link:
Playground
Related Issues:
#21756
Also, related to my comment here,
#23689 (comment)
The text was updated successfully, but these errors were encountered: