You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Typescript forbids safety checks that check equality of values with different generic types #17445
When a function is passed two arguments of two different generic types, there exists the possibility that the two generic types are in fact the same type and that the two values are the same value. A function should be able to test for this case and handle it appropriately, especially when ensuring the two values are different is essential to a safety condition. However, typescript forbids us from making this comparison.
Code
// 'Operator '===' cannot be applied to types 'T' and 'U'.'functiontest<T,U>(a: T,b: U): void{if(a===b){// The caller set T and U to the same type and 'a' and 'b' to the same value.// Take apprpriate action}}
Expected behavior:
Allow the comparison, perhaps with a TSLint warning asking me if I'm sure I want to do this.
Actual behavior:
'Operator '===' cannot be applied to types 'T' and 'U'.'
Forcing me to make workaround of
// Workaround using the three-letter profanity eschewed by all righteous TypeScript programmersfunctiontest<T,U>(a: T,b: U): void{if(a===(basany)){// The caller set T and U to the same type and 'a' and 'b' to the same value.// Take apprpriate action}}
This is something we should consider. I would say that the proposed change is the following:
*S* is ***comparable to*** a type *T*, and *T* is ***comparable from*** *S*, if one of the following is true:
* ...
* *S* is a type parameter and the constraint of *S* is comparable to *T*.
+ * *T* is a type parameter and *S* is comparable to the constraint of *T*.
Please no. There is a reason why the saying "comparing apples and oranges" exists. T and U are different types and comparison is meaningless in the general case. Casting to any is a sufficient workaround, IMHO.
Reacted by Herrington Darkholme and Junyoung/"Clare" Jang
Yeah, I agree, unless there is some sort of type relationship between the two, the type system is doing what a type system does, letting you know you might have a logic error, unless you are more specific.
interfaceFoo{foo: string;}interfaceBarextendsFoo{bar?: string;}functiontest<TextendsFoo,UextendsBar>(a: T,b: U){if(a===b){// Operator '===' cannot be applied to types 'T' and 'U'.// do something}}
I don't think it should be valid. The real type of equality is <T>(x: T, y: T) => boolean. I.e. only arguments of the same type should be compared. That's the reason why in Java the first thing you check is whether the classes of the arguments are the same. If the intention is to check Foo's descendants, then test(a: Foo, b: Foo) would be a better signature.
That's the reason why in Java the first thing you check is whether the classes of the arguments are the same.
TypeScript is not a nominal typing system. I know you know that. So comparisons to Java are grounded in folly. Types are assignable based on their structure, not their ancestry, so comparisons of two related types can end up with a variable having two different types but still be the same reference, something that is impossible in Java.
True though that test(a: Foo, b: Foo) is better solution to the particular problem here, but to disallow strict equality of references when the types are related would seem contrary to a structural type system to me.
In T extends Foo, U extends Bar, you clearly know that U has one property more. If it didn't, they'd be the same type (maybe with different aliases)! But in general, T can be any descendant, including ones not following the Bar line, and in that occasion you'd be comparing apples to oranges.
Another way to look at it is that bounds provide a minimal interface, but the concrete types are "some types" that we know nothing about. On the logical level we shouldn't be comparing two unknown things. And indeed, most of the time it makes no sense.
Do we really want to let the plethora of badly typed programs, for the rare single valid one? E.g. in the simplest case of number:
will always return false unless T and U are actually the same literal type.
PS: Please note that every type parameter can be weakened to the lower bound of {} | null | undefined. That means that any two parameters will be equitable, provided they don't have explicit incompatible bounds.
will always return false unless T and U are actually the same literal type.
Which the can be, quite easily, so why disallow the comparison? In that case it is entirely valid and entirely sound. Allowing the comparison doesn't make anything less sound, and it is arguable that it is statically identifying an error.
There is a big difference in comparing value/primitive based variables and non-primative/references. The type system doesn't make a huge distinction against them, but there are certainly cases where it is valid to strictly compare two references to see if they are in fact the same object, which is a different operation than comparing if two primitives are the same value.
TypeScript in this case is asserting something is unsound when it actually doesn't know if it is unsound or not. The TypeScript team generally err on the side of pragmatic about uncertainty, only getting in the way when it is certain constructs are errors. I would see an argument if it were some sort of assignment, but a comparison, worst case is unnecessary or illogical versus being wrong and in some cases is totally valid.
I'm a little confused about "unsound" here: JS doesn't let you override operators, so there's zero chance of a === b causing an error when run, or causing coercion like (all the?) other operatorrs. The only soundness errors TS could give here would be where the types are exclusive: (x: string, y: number) => x === y should probably error with something like 'string' and 'number' can never be equal.
If you wanted, you could make a pragmaticness argument though: (x: { foo: string }, y: { bar: number }) => x === y seems like it should be an error, but { foo: "evil", bar: 666 } is assignable to both x and y.
I would prefer it to be permissive here, but I don't feel too strongly.
I have a hard time with this. It's tempting to allow any comparison with ===, but it's very easy to write code like
if(x.name===entry.getName){// oops, forgot to call, this code is now unreachable}
or
if (input.value === 0) { // forgot to use parseInt or unary+
With no information about what T and U are, it seems foolish to allow comparisons between them without some indication from the developer that they intend to do this. What about code like this, where you have two completely isolated domains of values and want help keeping them separate?
functiondoMap<K,V>(x: MappingThing<K,V>){for(constkofx.keys()){constv=x.getValue(k);if(k===v){// wat, why is this allowed? keys aren't values!// ...}}}
in #18394 (comment) we decided that we don't think users will benefit as much from the sort of change involved here. We believe most instances of comparisons between two independent generics are likely to be mistakes, and using a type assertion as a workaround is an acceptable tradeoff.
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
FWIW, I frequently find this feature to be incredibly annoying - consider this simple example:
functionequal<A,B=A>(a: A,b: B){returna===b;}
I have something like that (but not exactly like that) in a real project with much more complex generic typings, where two types can be either the same (by default) or optionally may be different.
I think, in part, what bugs me is the error message:
Operator '===' cannot be applied to types 'A' and 'B'.
This is first of all not true - this being Javascript, you absolutely can apply the === operator to objects of different types.
The reason this message is even more annoying though, is that you don't even know if these type-arguments are of the same type or not - with type Bdefaulting to the same as type A in this example, it's more than just possible, it's actually quite likely.
Just raises questions for the person reading the code. "why a type-cast to any?" Most times when you see as any it's being used by beginners to shut the compiler up - in strict mode especially, where, once you get proficient with the type-system, there is hardly a valid use-case for as any that can't be fixed by properly working through your types, and the use of it becomes a giant red flag in code-reviews etc.
At the very least, the error message ought to provide a more meaningful explanation - don't tell me the operator "cannot be applied", it absolutely can. You could say something like "should not be applied" or "may not be applicable to types A and B", but it really is quite confusing to be told that the === operator doesn't work on differing types, this being JS, where it actually can applied just fine.
But ideally you should provide a more idiomatic means of dealing with this scenario - perhaps some means of annotating a type argument as being comparable to another? I don't know how you'd describe that feature, to be honeste.
But as any really seems like an ugly work-around - because that's usually all as any ever is.
if you want to do janky stuff in the function body.
This is first of all not true - this being Javascript, you absolutely can apply the === operator to objects of different types.
I mean, the entire point of TypeScript is to forbid things which are legal JavaScript but not what the programmer intended. It's allowable to write window === new ArrayBuffer() but that doesn't make any sense and it's good for us to disallow it.
I'm also not clear on what "this function takes two arguments that might be the same but might not be" means.
I'm over-simplifying, it's far more complex in my real use-case :-)
Anyhow, the visible vs implementation signature work-around - nice! I hadn't thought of that. It's much less a work-around than as {}, I think, and won't raise any confusion/questions. I'll take it! thanks! :-)
Activity
DanielRosenwasser commentedon Aug 2, 2017
This is something we should consider. I would say that the proposed change is the following:
*S* is ***comparable to*** a type *T*, and *T* is ***comparable from*** *S*, if one of the following is true: * ... * *S* is a type parameter and the constraint of *S* is comparable to *T*. + * *T* is a type parameter and *S* is comparable to the constraint of *T*.gcnew commentedon Aug 2, 2017
Please no. There is a reason why the saying "comparing apples and oranges" exists.
TandUare different types and comparison is meaningless in the general case. Casting toanyis a sufficient workaround, IMHO.kitsonk commentedon Aug 2, 2017
Yeah, I agree, unless there is some sort of type relationship between the two, the type system is doing what a type system does, letting you know you might have a logic error, unless you are more specific.
This should be valid though, but isn't:
gcnew commentedon Aug 2, 2017
I don't think it should be valid. The real type of equality is
<T>(x: T, y: T) => boolean. I.e. only arguments of the same type should be compared. That's the reason why in Java the first thing you check is whether the classes of the arguments are the same. If the intention is to check Foo's descendants, thentest(a: Foo, b: Foo)would be a better signature.gcnew commentedon Aug 2, 2017
Related: #17404
kitsonk commentedon Aug 2, 2017
TypeScript is not a nominal typing system. I know you know that. So comparisons to Java are grounded in folly. Types are assignable based on their structure, not their ancestry, so comparisons of two related types can end up with a variable having two different types but still be the same reference, something that is impossible in Java.
True though that
test(a: Foo, b: Foo)is better solution to the particular problem here, but to disallow strict equality of references when the types are related would seem contrary to a structural type system to me.gcnew commentedon Aug 2, 2017
In
T extends Foo, U extends Bar, you clearly know thatUhas one property more. If it didn't, they'd be the same type (maybe with different aliases)! But in general,Tcan be any descendant, including ones not following theBarline, and in that occasion you'd be comparing apples to oranges.Another way to look at it is that bounds provide a minimal interface, but the concrete types are "some types" that we know nothing about. On the logical level we shouldn't be comparing two unknown things. And indeed, most of the time it makes no sense.
Do we really want to let the plethora of badly typed programs, for the rare single valid one? E.g. in the simplest case of
number:will always return
falseunlessTandUare actually the same literal type.PS: Please note that every type parameter can be weakened to the lower bound of
{} | null | undefined. That means that any two parameters will be equitable, provided they don't have explicit incompatible bounds.kitsonk commentedon Aug 2, 2017
Which the can be, quite easily, so why disallow the comparison? In that case it is entirely valid and entirely sound. Allowing the comparison doesn't make anything less sound, and it is arguable that it is statically identifying an error.
This is totally valid:
There is a big difference in comparing value/primitive based variables and non-primative/references. The type system doesn't make a huge distinction against them, but there are certainly cases where it is valid to strictly compare two references to see if they are in fact the same object, which is a different operation than comparing if two primitives are the same value.
TypeScript in this case is asserting something is unsound when it actually doesn't know if it is unsound or not. The TypeScript team generally err on the side of pragmatic about uncertainty, only getting in the way when it is certain constructs are errors. I would see an argument if it were some sort of assignment, but a comparison, worst case is unnecessary or illogical versus being wrong and in some cases is totally valid.
simonbuchan commentedon Aug 8, 2017
I'm a little confused about "unsound" here: JS doesn't let you override operators, so there's zero chance of
a === bcausing an error when run, or causing coercion like (all the?) other operatorrs. The only soundness errors TS could give here would be where the types are exclusive:(x: string, y: number) => x === yshould probably error with something like'string' and 'number' can never be equal.If you wanted, you could make a pragmaticness argument though:
(x: { foo: string }, y: { bar: number }) => x === yseems like it should be an error, but{ foo: "evil", bar: 666 }is assignable to bothxandy.I would prefer it to be permissive here, but I don't feel too strongly.
RyanCavanaugh commentedon Aug 8, 2017
I have a hard time with this. It's tempting to allow any comparison with
===, but it's very easy to write code likeor
With no information about what
TandUare, it seems foolish to allow comparisons between them without some indication from the developer that they intend to do this. What about code like this, where you have two completely isolated domains of values and want help keeping them separate?simonbuchan commentedon Aug 9, 2017
Perhaps must share a not-top type? Or one is assignable to the other?
9 remaining items
DanielRosenwasser commentedon Sep 19, 2017
To quote myself from #18530 (comment)
mhegazy commentedon Oct 4, 2017
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
mindplay-dk commentedon Mar 28, 2018
FWIW, I frequently find this feature to be incredibly annoying - consider this simple example:
I have something like that (but not exactly like that) in a real project with much more complex generic typings, where two types can be either the same (by default) or optionally may be different.
I think, in part, what bugs me is the error message:
This is first of all not true - this being Javascript, you absolutely can apply the
===operator to objects of different types.The reason this message is even more annoying though, is that you don't even know if these type-arguments are of the same type or not - with type
Bdefaulting to the same as typeAin this example, it's more than just possible, it's actually quite likely.And the work-around is quite confusing:
Just raises questions for the person reading the code. "why a type-cast to
any?" Most times when you seeas anyit's being used by beginners to shut the compiler up - in strict mode especially, where, once you get proficient with the type-system, there is hardly a valid use-case foras anythat can't be fixed by properly working through your types, and the use of it becomes a giant red flag in code-reviews etc.At the very least, the error message ought to provide a more meaningful explanation - don't tell me the operator "cannot be applied", it absolutely can. You could say something like "should not be applied" or "may not be applicable to types A and B", but it really is quite confusing to be told that the
===operator doesn't work on differing types, this being JS, where it actually can applied just fine.But ideally you should provide a more idiomatic means of dealing with this scenario - perhaps some means of annotating a type argument as being comparable to another? I don't know how you'd describe that feature, to be honeste.
But
as anyreally seems like an ugly work-around - because that's usually allas anyever is.RyanCavanaugh commentedon Mar 28, 2018
Rasmus Schultz (@mindplay-dk) I like to recommend people upcast when they're doing something non-suspicious:
as {}is a lot less scary because you can be sure you're not accidently spreading an infectiousanythrough your codeRyanCavanaugh commentedon Mar 28, 2018
Also, the implementation signature and the visible signature need not be exactly the same. You can write
if you want to do janky stuff in the function body.
I mean, the entire point of TypeScript is to forbid things which are legal JavaScript but not what the programmer intended. It's allowable to write
window === new ArrayBuffer()but that doesn't make any sense and it's good for us to disallow it.KiaraGrouwstra commentedon Mar 28, 2018
with
strictNullChecks, the above signature disallows this:I tend to skip the generics for functions like this, as they tend to give false positives.
RyanCavanaugh commentedon Mar 28, 2018
I'm also not clear on what "this function takes two arguments that might be the same but might not be" means. Isn't that just...?
mindplay-dk commentedon Apr 18, 2018
I'm over-simplifying, it's far more complex in my real use-case :-)
Anyhow, the visible vs implementation signature work-around - nice! I hadn't thought of that. It's much less a work-around than
as {}, I think, and won't raise any confusion/questions. I'll take it! thanks! :-)