-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Flow analysis doesn't work with es6 collections 'has' method #13086
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
Comments
@MastroLindus
|
I wrote up a fun little hack that uses const x = new Map<string, string>();
interface Map<K, V> {
has<CheckedString extends string>(this: Map<string, V>, key: CheckedString): this is MapWith<K, V, CheckedString>
}
interface MapWith<K, V, DefiniteKey extends K> extends Map<K, V> {
get(k: DefiniteKey): V;
get(k: K): V | undefined;
}
x.set("key", "value");
if (x.has("key")) {
const a: string = x.get("key"); // works!
} Unfortunately these don't stack perfectly - you can't write |
Actually, you can get this to work better with another overload. const x = new Map<string, string>();
interface Map<K, V> {
// Works if there are other known strings.
has<KnownKeys extends string, CheckedString extends string>(this: MapWith<string, V, KnownKeys>, key: CheckedString): this is MapWith<K, V, CheckedString | KnownKeys>
has<CheckedString extends string>(this: Map<string, V>, key: CheckedString): this is MapWith<K, V, CheckedString>
}
interface MapWith<K, V, DefiniteKey extends K> extends Map<K, V> {
get(k: DefiniteKey): V;
get(k: K): V | undefined;
}
x.set("key", "value");
if (x.has("key") && x.has("otherKey")) {
const a: string = x.get("key"); // works!
const b: string = x.get("otherKey"); // also works!
} |
@DanielRosenwasser I think you're a bit overcomplicating the stuff. const map = new Map<string, string>();
const value = <string>(map.has('key') ? map.get('key') : 'default value'); Anyway, these all are workarounds. |
@DanielRosenwasser is on the cards for being fixed via flow analysis? It feels like there are some workarounds but no acknowledgment whether this is intended or needs fixing. |
Just stumbled upon this issue, wanted to note that this is quite hard to get right. For example the workarounds do not take into account that the entry might be |
@jeysal that's true, but I believe 90% of use cases are just |
This is such common use case—I was surprised it's not covered! Fortunately, as others have mentioned, it's easy to work around the gap. To the argument of complexity, would the static analysis here not be much simpler given that data access in ES2015 collections is much more straightforward compared to, for example, arrays? array.indexOf(key) >= 0 // O(n)
set.has(key) // O(1) |
This much, much, much, much more tricky than it looks because you also have to define how long the |
@chapterjason you could do const factory = this.factories.get(name)
if(factory)
return factory;
throw new ArgumentException("..."); |
Wouldn't you have the exact same problem with an object index lookup when the members on the object are optional? Does TypeScript handle that case correctly? |
Will the awesome new control flow analysis in TypeScript beta 4.4 make this possible/easier? |
It's possible to implement this way, but it's as verbose as loading the value and checking if it's defined. class OutBox<V> {
public value!: V
constructor() {}
static empty<V>() {
return new OutBox<V>()
}
}
class PowerfulMap<K, V> extends Map<K, V> {
getOrThrow(key: K): V {
return this.get(key) ?? throw new Error('where throw expressions???')
}
getIfHas(
key: K,
outBox: OutBox<V | undefined>,
): outBox is OutBox<NonNullable<V>> {
outBox.value = this.get(key)
return this.has(key)
}
}
class Foo {
someProp = 'a'
}
const map = new PowerfulMap<string, Foo>()
const box = OutBox.empty<Foo | undefined>()
if (map.getIfHas('hello', box)) {
console.log(box.value.someProp) // guarded
} else {
console.log(box.value.someProp) // no guard
} |
or... just this (Playground): class XMap<K, V> extends Map<K, V> {
getThen<T>(key: K, map: (value: V) => T): T | undefined {
if (this.has(key)) {
return map(this.get(key)!);
}
return;
}
}
const m = new XMap([['a', 1]]);
// (of course you can do all computation in the callback)
console.log('mapped v', m.getThen('a', v => { console.log('v', v); return v + 1; }));
console.log('mapped v (2)', m.getThen('b', v => { console.log('v (2)', v); return v + 1; })); |
it's frustrating to see the mediocrity of some api in js
|
map#find I think will be added in the iterators protocol |
By the way this capability could be pretty useful in Solid.js codebases, where you could write code like Though obviously it's unclear how to invalidate this perfectly. |
this is impossible, Typescript doesn't know what |
do you really want to go back to callback hell? |
Is there a reason why I feel like their applications are very similar |
It has been seven years and the thing clearly designed to be used as a type guard, still isn't treated as a type guard. Is there a reason that this isn't built in or has it just never been a priority? |
On Wed, 16 Aug 2023 at 00:37, Connor Brennan ***@***.***> wrote:
It has been seven years and the thing clearly designed to be used as a
type guard, still isn't treated as a type guard. Is there a reason that
this isn't built in or has it just never been a priority?
—
Reply to this email directly, view it on GitHub
<#13086 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ALRQ7U2DED3TJ5NOOW2W7QLXVPCFJANCNFSM4C2KVZUQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
It is not clearly designed to be a type guard. It’s an existence check for
the key. The existence of the key doesn’t necessarily imply that the value
is the type you expect it to be. You want that? Write a type guard directly
for the value.
|
I phrased that poorly. What I intended was that it is designed to work as an undefined check, and should be treated as a type guard with a generic type, so that after its use the compiler treats the value as definitively not undefined.
Should not have bar throwing "possibly undefined" issues, since we should know for certain it is not undefined. |
The value stored at the key could be undefined. import invariant from 'ts-invariant'
if(mapOne.has('foo')) {
let bar = mapOne.get('foo')
invariant(bar) // no-op or remove in prod build
}
let bar = mapOne.get('foo')
if (bar) {
...
} What's wrong with either of these approaches? |
That's a pretty terrible example because const mapExample = new Map<"foo"| "bar", number>()
mapExample.set("foo", 1)
mapExample.set("bar", undefined) But typescript is still unsure of the value: if (mapExample.has("foo")) {
const val = mapExample.get("foo")
} else {
const val = mapExample.get("foo")
} In both branches it is |
Why bother with compile-time data assumptions when you can... wait for it.... use a runtime type guard with the same LOC. |
By the same logic you have to use type guards for any collection property access because you never know if one returns |
if I may add, by the very same logic you might as well write type guards for every variable no matter the type, because you never know if someone decided to shoehorn an Typescript is 100% compile-time. If you don't trust compile-time type checks, why use Typescript at all? |
No, the thing comes into play here because a check for the truthiness of the value is necessary. I'm saying might as well go with the existing type guards at that point as opposed to trying to modify the language in a way that I'm not sure even is possible. if (mapExample.has("foo")) {
const val = mapExample.get("foo")
mapExample.delete("foo")
mapExample.get("foo") // whats the type here?
} So you are saying the compiler has to add special case to detect that these two method calls add and delete values from the values that can be returned from the If you have reason to believe string variables can be polluted due to I/O, yes by all do a runtime check on them. Regarding de-reffing array variables, yes, you have to turn on the |
i find alternative hack for my usecase but your eyes will bleeding |
@djmisterjon that only works if your keys are distinct types, if your key is in a |
Yet it will infer the type of variables declared with
I wasn't talking only about arrays, objects are collections too. You have to validate the value of every single dot/index notation access (do note the key might be a getter, so you have to call with |
What? What does truthiness have to do with any of this? As for your snippet: if (mapExample.has("foo")) { // should narrow the type
const val = mapExample.get("foo"); // the type here is NOT undefined
mapExample.delete("foo"); // should narrow the type again
mapExample.get("foo"); // the type here is undefined
}
I beg to differ: interface Cat {
name: string;
breed?: string;
}
const cat = {} as Cat;
doStuff(cat.breed); // why is there an error here
if(cat.breed !== undefined) {
doStuff(cat.breed); // but not here?
}
function doStuff(breed: string): void {} I don't see how a map should be different from an object. Internally, they're both hash maps, they just have different APIs for accessing their members (and Map also maintains an ordered index of entries, but that's irrelevant). In both cases someone can technically use type casting or simply vanilla Javascript to inject values that Typescript does not expect. Nevertheless, that doesn't stop Typescript in my above example from correctly assuming that the internal state of object/hashmap |
Yet another workaround helper, which leverages type predicates: function has<K, T extends K>(set: ReadonlySet<T> | ReadonlyMap<T, any>, key: K): key is T {
return set.has(key as T);
} Some tests: describe('a const Set', () => {
const set = new Set(['foo', 'bar'] as const);
it('checks a key outside the set', () => {
expect(has(set, 'lorem')).to.be.false;
});
it('checks a key inside the set', () => {
expect(has(set, 'foo')).to.be.true;
});
it('narrows a superset type', () => {
function test(key: 'foo' | 'bar' | 'baz'): void {
if (has(set, key)) {
// @ts-expect-error :: never true
if (key === 'baz') return;
} else {
// @ts-expect-error :: never true
if (key === 'foo') return;
}
}
test('baz');
});
it('has type errors if the types do not overlap', () => {
// @ts-expect-error :: key can never be valid
expect(has(set, 123)).to.be.false;
});
}); Would love for this to be built-in. |
TypeScript Version: 2.1.1
Code
Expected behavior:
y is narrowed down to string
Actual behavior:
y is still string | undefined even after checking if the map has that key
The text was updated successfully, but these errors were encountered: