-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Typescript forbids safety checks that check equality of values with different generic types #17445
Comments
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. |
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: interface Foo {
foo: string;
}
interface Bar extends Foo {
bar?: string;
}
function test<T extends Foo, U extends Bar>(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 |
Related: #17404 |
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 |
In 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 function testNumber<T extends number, U extends number>(x: T, y: U) {
return x === y;
} will always return PS: Please note that every type parameter can be weakened to the lower bound of |
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: function testNumber(x: number, y: number) {
return x === y;
} 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 If you wanted, you could make a pragmaticness argument though: 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 if (x.name === entry.getName) {
// oops, forgot to call, this code is now unreachable
} or
With no information about what function doMap<K, V>(x: MappingThing<K, V>) {
for (const k of x.keys()) {
const v = x.getValue(k);
if (k === v) { // wat, why is this allowed? keys aren't values!
// ...
}
}
} |
Perhaps must share a not-top type? Or one is assignable to the other? |
@RyanCavanaugh, in the two examples you give the two objects are of different native types and guaranteed to be different. Yielding an error there seems to be the correct behavior. Is it not possible to handle the case of a comparison between two generic types, for which it is unknown whether objects of the same type may be assigned to both and thus unknown in advance whether they are equal, differently? |
I think Ryan is arguing, and I agree with him, 99% of the time you have two generics, they are so unlikely to strict equal each other. Being technically correct and being pragmatic in helping the user are two different things. If you have real world code, that leverages generics properly, and disallowing function test<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
}
} In this particular case, because the generics don't actually do anything, aren't really used in the code, etc. it would be a lot better (and ultimately safer) to write it this way: function test(a: any, b: any): void {
if (a === b) {
//
}
} |
@kitsonk Any case that would hit this issue would probably be some variant of this. class Component<T = any> {
parent: null | Component;
children: Component[];
props: T; // Makes Component<T> and <U> different types.
replaceWith<U>(other: Component<U>): Component<U> {
if (this === other) return other; // '===' cannot be applied to `this` and `Component<U>`
if (!this.parent) return other;
return this.parent.replaceChild(this, other);
}
replaceChild<U>(existing: Component, replacement: Component<U>): Component<U> {
const index = this.children.indexOf(existing);
if (index < 0) {
throw new RangeError('Invalid child.');
}
if (this.children[index] === replacement) { // OK: LHS T is 'any'
return replacement;
}
this.children[index] = replacement;
this.expensiveUpdateChild(index);
return replacement;
}
expensiveUpdateChild(index: number): void {
// ...
}
} |
@simonbuchan yes, you are essentially attempting to work around #6223 (Polymorphic |
i say asking to enable comparison between 2 generics // Right way of doing it
function test<T, U>(a: T, b: U, areEqual: (a: T, b: U) => boolean): void {
if (areEqual(a, b)) {
// Since at this point we don't know what T and U are, we can't make any assumptions how they
// can be compared to each other, that's why we ask the caller to provide the comparison function
// Take apprpriate action
}
} a side note, when you write generic code you can't make any assumptions as to what will be used as type arguments, with that in mind the only way to manipulate those generics is to request necessary operations to be passed from the outside (including but not limiting to @simonbuchan hardcoding
|
This doesn't really mean anything, because in this alternative formulation we're simply asking for |
@masaeedu Following this logic, all invokations should be allowed with Basically |
I tried a PR at #18530 that uses This is a conservative version of @DanielRosenwasser's proposal to generally add the rule of To elaborate on how this would handle the different scenarios here, cutting out most of the code for brevity's sake:
@RyanCavanaugh has a point on the current behavior potentially preventing unintended behavior in the I wouldn't say we can read the user's mind to always detect their intention, but the confusion primarily exists when one side of the comparison has no constraint (and therefore could equal things like keys). Without a road of perfection, I'd argue that's the smallest available sacrifice.
Agreed. That seems orthogonal to the present topic though (that is, type variables not getting resolved to their constraint). Generics aren't the enemy here; I'd rather we improve on both issues than on neither. |
I'd agree with
They are different types, but types are a synthetic construction in a type system with structural subtyping; the same value may inhabit any number of types. |
To quote myself from #18530 (comment)
|
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: function equal<A, B = A>(a: A, b: B) {
return a === 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:
This is first of all not true - this being Javascript, you absolutely can apply the 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 And the work-around is quite confusing: function equal<A, B>(a: A, b: B) {
return a === b as any; // huh?
} Just raises questions for the person reading the code. "why a type-cast to 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 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 |
@mindplay-dk I like to recommend people upcast when they're doing something non-suspicious: function equal<A, B>(a: A, b: B) {
return a === b as {}; // nothing up my sleeve
}
|
Also, the implementation signature and the visible signature need not be exactly the same. You can write function equal<A, B extends A = A>(a: A, b: B): boolean;
function equal(a: {}, b: {}) {
return a === b;
} 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 |
with
I tend to skip the generics for functions like this, as they tend to give false positives. |
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...?
|
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 |
TypeScript Version: 2.4.2
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
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
The text was updated successfully, but these errors were encountered: