-
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
Types number
and explicitly constrained T extends unknown
shouldn't be comparable
#32814
Comments
number
and T extends unknown
shouldn't be comparablenumber
and explicitly constrained T extends unknown
shouldn't be comparable
I would expect both the explicit and implicit It's sound that if If it's disallowed, does that mean the code would then have to be, const num = 1;
function check<T extends unknown>(x: T) {
return typeof x === "number" && x === num;
}
check(num); to work? |
I suppose the following shouldn't be allowed either: function check<T extends string | number>(x: T) {
return x === 1;
} The problem is not specific to |
This seems backwards to me; it's clear that "This condition will always return 'false'" is a lie. In the example, it will return The current |
@davidje13 The problem with that approach is that any two unrelated type parameters become comparable, which is a bug in many cases. I'll probably wait on the design meeting before looking more into this --- it's not quite clear what the new behaviour should be. The code-path I linked on the other issue is the one influencing the unconstrained case, which means that the implicit constraint of
There may be more options I don't know about. |
For what it's worth, there was a suggestion that "any two type parameters should be strictly rejected when comparing them", but the fact that that wouldn't cover |
It feels like you're trying to shoehorn code quality checks into a type system. If you make it behave this way, you will be breaking the correctness of the type checking and you'll be chasing weird edge cases throughout the system (not to mention forcing the use of To be clear, it is obvious that Of course, TypeScript should catch |
I don't know what breaking the correctness of type checking means in this context, but it would not force users to use function compare(x: unknown, y: unknown) {
return x === y;
}
function test<T,U>(t: T, u: U) {
if (compare(t,u)) {
}
}
There are plenty of languages that do not let you compare arbitrary generic types, so I'm not sure what you mean by 'wrong'. |
@jack-williams see the original issue #32768 for my actual use-case which kicked this off. There's clearly some miscommunication here because both of us think our view is the only possible logical interpretation. So to maybe clarify my perspective: function compare<T, U>(t: T, u: U) {
return t === u;
} What we know before the comparison:
What we additionally know if the comparison is true:
What we additionally know if the comparison is false:
Clearly the The
Which resolves to saying that ∃ S : S ⊆ ( At this point, we do not have enough information to formally decide this one way or the other; ( What I mean by "breaking the correctness of the type checking" is exactly this; if we choose to assume undecidable states cannot happen, we are no-longer modelling how the program can actually operate (i.e. the real behaviour it may exhibit). The only way to continue modelling how the program can actually operate is by assuming that undecidable states can happen, until we have enough information to prove that they cannot happen (at which point they cease being undecidable). To hammer the point home, consider the closely related code: function compare(t: string | number, u: string | number) {
return t === u;
} Once again, |
Useful, consistent, correct: Pick any two 😅 |
An additional desirable symmetry is that a generic function should be type checked such that an error is issued if any concrete version of the function would have errored. In this case, we can easily construct an erroring instatiation
Moreover, you have many options when writing |
That's actually a really good argument. And now that I'm thinking about it that way, I'm having trouble coming up with an example where The original issue's intent would have been better written as, const isNum = (v: unknown) => {
if (typeof v !== 'number') throw new Error('Not a number');
return v;
};
const isString = (v: unknown) => {
if (typeof v !== 'string') throw new Error('Not a string');
return v;
};
function makeExampleValue<T extends number|string>(validator: (v: unknown) => T): T {
if (validator === isNum) return 42 as T;
if (validator === isString) return 'something' as T;
//Not really "unknown type".
//More like, "unknown validator", since we're doing function comparisons
throw new Error('unknown type');
} |
Is the following behaviour intentional? function check<T extends string|number> (t : T) {
//This is okay, even though the instantiation of `T`
//with `string` should make it non-comparable.
//But becaues we are comparing a generic to a non-generic,
//we seem to use the constraint `string|number`
//and check if it is comparable to the non-generic `number`
return t === 1;
}
function check2<T extends string|number, U extends number> (t : T, u : U) {
//Doesn't work, though.
//Cannot compare generic type `T` and `U`
//because `T` could be `string`
//and `U` could be `number`
return t === u;
} [Edit] [Edit]
Should be more like,
Would be even better if it could say,
|
From a purely theoretical standpoint: function foo<T>(x: T): boolean
{
return x === 1; // should this be an error?
}
However, inside the function,
But this is not necessarily sound. Because the function as a whole is universally quantified,
Which seems to be what @RyanCavanaugh is getting at above (minus the nerdiness 🤓). |
@RyanCavanaugh I understand your viewpoint but I disagree. @fatcerberus that is true for assignments or casts, etc. but not for comparison. The equality operator is, after all, able to compare anything with anything else, so even |
@davidje13 Just for the record: So by your argument, the above illustrated error needs to be removed too. |
@fatcerberus no that's not what I'm saying (I updated my comment to add "at runtime" just as you posted that to clarify that exact point) My point is that type-wise, the check is fine, but in that case the error is 100% correct about the reachability. It can be proven at compile time that the check will, guaranteed, be My comment about The discussion here is about what to do when it cannot be determined at compile time whether it will always be true, or always be false |
FWIW I hadn't really expressed my view on the matter, I was just trying to lay out the consequences of the alternatives.
If I understand your description correctly you are describing completeness of type-checking, but this is not a property that most type-checkers ever try to satisfy---so I disagree that this is breaking any correctness criteria that TypeScript should ever realistically aspire to. |
I guess what I most don't understand is why these functions, as written, are generic. Maybe it's just the danger of simplified repros, but since function foo(x: unknown): boolean
{
return x === 1; // no error
}
function isSame(x: unknown, y: unknown): boolean
{
return x === y; // also no error
} Since you only care about equality here and not exactly what the types involved are, there's no reason to take a type parameter at all. You can use the constraint directly, whatever it happens to be. Generics are mainly for when you need to use the type parameter to more precisely type something else (such as the return value), which doesn't seem to be happening here. Of course it's probably just that the real case is more complex and really does need to be generic, in which case this post would just be noise. If so I apologize--just want to cover all the bases. |
The problem here is an issue of "caller" vs "implementer" and what you know vs what you don't know. To the caller, If it knew, it would give you errors, too. To the implementer, It doesn't know that That function isSame<T, U extends T>(t: T, u: U) {
return t === u;
} This way, it doesn't matter what |
I think this will be my last comment on this issue, because it doesn't affect me too strongly (I can always use function mapReversed<T, U>(source: T[], target: U[], map: (t: T) => U): void {
const count = source.length;
if (target.length !== count) {
throw new Error('target length mismatch');
}
if (source === target) {
// cannot reverse in-place, so perform extra steps
// (uses more memory, builds up garbage for the gc to handle, and is slower, but guarantees correctness)
const mapped = source.map(map);
for (let i = 0; i < count; i++) {
target[count - i - 1] = mapped[i];
}
} else {
// reversing in-place is fine; use optimised code (no garbage generated for gc)
for (let i = 0; i < count; i++) {
target[count - i - 1] = map(source[i]);
}
}
} |
Ah right, it’s an optimization in a larger generic function. Yeah, optimizations like that are generally one case where you have to go over the type system’s head at some point—speaking from experience. Probably a case for |
[Edit] |
Everyone's raised some good points and I really appreciate the discussion here. 👍 |
I think this is the same problem (more specifically, the same as #32768 which is a duplicate of this): function isEmpty<T>(thing: T, key: keyof T) {
// This condition will always return 'false' since the types 'T[keyof T]' and 'string' have no overlap.
return thing[key] === ''
} Unfortunately, the workaround (which it sounds like is a bug) of |
Expected: Error: This condition will always return 'false' since the types 'T' and '1' have no overlap.
Actual: No error.
Contrast this with the following example from #32768.
The text was updated successfully, but these errors were encountered: