-
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
Set.has()
should accept any value, and could be a type predicate
#60547
Comments
Set.has()
into an inferred type predicateSet.has()
should be an inferred type predicate
Set.has()
should be an inferred type predicateSet.has()
should accept any value, and could be a type predicate
Basically the same as #26255; we don't agree that making |
Also see #51678 and issues linked from there |
Thank you for the quick response!
Well, But isn't To get the best of both worlds, I think you need some way of enforcing non-disjointness. After creating the function contains<
<<__NonDisjoint>> T1,
<<__NonDisjoint>> T2
>(
readonly Traversable<T1> $traversable,
readonly T2 $value,
)[]: bool; TypeScript may have a more elegant way of expressing this, using conditional types. Sorry, yes. I didn't realize that type guards work both ways. |
While not completely ideal, a type that can mostly fit the utility of // `[Extract<T, U>, any] extends [U, Extract<T, U>]` simultaneously checks if `Extract<T, U>` extends `U` and
// also that `Extract<T, U>` is not `never`.
// Then `U extends T ? T : U` is a useful fallback to allow stuff like `unknown` as `Extract<T, U>` will still leave `unknown` which will rarely extend `U`.
export type OverlapsWith<T, U> = [Extract<T, U>, any] extends [U, Extract<T, U>] ? T : (
U extends T ? T : U
);
declare global {
interface Set<T> {
has<const V>(value: OverlapsWith<V, T>): boolean;
}
}
const s = new Set([[1, 2, 3], [4], [5, 6]]);
// Works whether this overload is added or not.
s.has([1, 3]);
s.has(Object.assign({}, [1, 3], { prop: 123 }))
// Always erroring as it should.
s.has(["foo", "bar"]);
s.has([1, "foo"] as const);
// Newly functioning
s.has(Math.random() ? [1, 3] : ["foo", "bar"]);
// This newly functions as its type `(string | number)[]` at runtime _could_ be all numbers.
s.has([1, "foo"]);
const x: unknown = ["foo", "bar"];
s.has(x);
// Note that this example is misleading:
const y: number[] | string[] = ["foo", "bar"];
s.has(y); // Errors but only because if you check the type of `y` is now `string[]` despite the explicit type annotation. It's a little less implicit but it should have the same power as I think it becoming a built-in type would be ideal for better errors. Plus I doubt that the TypeScript team would want to add this conditional type to the library files today, the requirement of making it generic is a bit strange at a glance and the errors can be a bit subpar in some cases. |
Yeah, |
Yes! We are on the same page! For completeness, here's a playground example showing that the typechecker is more forgiving towards // Good! (Type error in the right place with a really useful error message)
function arrayIncludesLoopDisjoint<T1, T2>(array: T2[], value: T1): boolean {
for (const x of array) {
// This comparison appears to be unintentional because the types 'T2' and 'T1' have no overlap.(2367)
if (x === value) {
return true;
}
}
return false;
}
// Good! (No type error)
function arrayIncludesLoopNonDisjoint<T1, T2 extends T1>(array: T2[], value: T1): boolean {
for (const x of array) {
if (x === value) {
return true;
}
}
return false;
}
// Makes me sad... (Type error, even though the equivalent code above typechecks. Error is kind of cryptic.)
function arrayIncludesBuiltin<T1, T2 extends T1>(array: T2[], value: T1): boolean {
// Argument of type 'T1' is not assignable to parameter of type 'T2'.
// 'T2' could be instantiated with an arbitrary type which could be unrelated to 'T1'.(2345)
return array.includes(value);
} |
⚙ Compilation target
ES2015
⚙ Library
lib.es2015.collection.d.ts
Missing / Incorrect Definition
TypeScript/src/lib/es2015.collection.d.ts
Lines 87 to 90 in d85767a
I believe
should become
This applies for other methods as well, such as
Map<K, V>.has(key: K): boolean
, as well as other ES standards.Essentially, it's not an error to pass an arbitrary value to
Set.has()
. It doesn't lead to a crash, exception, or error of any sort, even when we pass weird values like{}
,[]
, orNaN
, so the typechecker shouldn't stop us from doing it. We simply get atrue
if the value is in the Set, or afalse
if the value is not. And since theSet
can only contain values of typeT
, the boolean returned bySet.has()
also becomes proof thatvalue
is of typeT
. This makes it an ideal candidate for a type predicate.This would immediately fix several regressions in our codebase that we saw when we upgraded TypeScript from 5.3 to 5.5. TypeScript's inferred type predicates are amazing, but they narrowed the types of our Sets and caused several of our
Set.has()
method calls to become errors. The sample code is a simplified example of what we've found in the wild in our codebase.Sample Code
Documentation Link
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has
The text was updated successfully, but these errors were encountered: