-
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
Strict null checks for Map members #9619
Comments
the issue is not type guards, since the type of the map does not change between he has and the get calls. the issue is relating two calls. you want to tell the compiler the output of get is known to be undefined, because a call to has was successful. i am not sure i see how this can be done given the current system. |
This effectively forces users to add the postfix A. Type inference for return values is no longer reliable. Given that null or undefined can't be automatically stripped in certain cases, like above, one can get an incorrect return type. Because the behavior is unpredictable, this means one has to always write the return type annotations and not use type inference. This is very inconvenient. Example: function getNumber(map: Map<string, number>, key: string, defaultValue: number) {
if (map.has(key)) {
return map.get(key);
}
return defaultValue;
} The inferred return type is B. The explicit |
the alternative is to change the definition of Map, to be less strict, and assume that users will always call has appropriately. |
People can already augment the |
@RyanCavanaugh , changing the Going a bit off-topic, in my personal project, from 100+ migration errors from 'strictNullChecks', only 2-3 were actually relevant and only appeared in error cases anyway. Others were just things the compiler didn't figure out by itself (I use For the work project, it would make more sense to add this. However, most developers will struggle at first with the issues regarding lambda functions and they will also tend to overuse the |
As we (@dojo) have converted more of our code over, we are finding when using things like I wonder how difficult it would be to allow an expressing, like a custom type guard that would allow you to expressing something that CFA would be able to track... like maybe something like: interface Map<K, V> {
has<L extends K>(key: L): L is V;
get<L extends K>(key: L): V | undefined;
} Where where CFA could be fairly narrow in its type inference and if it see that same literal type, in the same block, it assumes the type on the right (which would eliminate |
Hope the sound improvement to avoid manually casting its non-null. |
I like that it just assumes by default that it may be undefined. But in a case like: type Key = 'foo' | 'bar';
const map = new Map<Key, number>([
['foo', 1],
['bar', 2],
]);
map.get('foo').toExponential();
^^^^^^^^^^^^^^ TS: Object is possibly 'undefined' Could at least take the keys from the constructor for granted. |
@sod I also prefer using typescript with strict null check. But here is a strange thing, I tried code below in typescript playground. const map = new Map<string, number>();
const a: number = map.get('foo'); // map.get might return `number | undefined` I expect a type check error, but it seems passed typescript type check. Is this because strict null check is not enabled in typescript playground? |
@kitsonk Got it. Didn't notice, my bad... |
One solution is to use a construct similar to swift's optional binding where you assign a variable in the const map = new Map<string, number>();
let val: ReturnType<typeof map.get>;
if ( val = map.get('foo')) {
// here val is type guarded as number since if statement
// only enters if it's not undefined.
val.toExponential();
} |
As for a solution that makes interface GuardedMap<K, T, KnownKeys extends K = never> extends Map<K, T> {
get(k: KnownKeys): T;
get(k: K): T | undefined;
has<S extends K>(k: S): this is GuardedMap<K, T, S>;
} This works great for string literals and sometimes with enums but has enough odd behaviour that definitely should not be implemented as main Map type: let map: GuardedMap<string, number> = new Map();
// Works for string literals!
if (map.has('foo')) {
// works for string literals!
map.get('foo').toExponential();
// this is identified as maybe being undefined!!
map.get('bar').toExponential();
}
let x = 'foo'; // typeof x === string
if (map.has(x)) {
// in here all strings are considered known keys.
map.get(x).toExponential();
// no error D:
map.get('bar').toExponential();
}
// lets say we end up with a variable with type never by mistake
let n = 0 as never;
// this is now considered type guarded??
map.get(n).toExponential(); |
@tadhgmister I was thinking along the same line when reading this issue. You can forbid dynamic access altogether with a conditional type: type GetKnownKeys<G extends GuardedMap<any, any, any>> = G extends GuardedMap<any, any, infer KnownKeys> ? KnownKeys: never;
interface GuardedMap<K, T, KnownKeys extends K = never> extends Map<K, T> {
get(k: KnownKeys): T;
get(k: K): T | undefined;
has<S extends K>(k: S): this is GuardedMap<K, T, (K extends S ? never : S)>;
}
let map: GuardedMap<string, number> = new Map();
// Works for string literals!
if (map.has('foo')) {
map.get('foo').toExponential();
map.get('bar').toExponential();
}
let x = 'foo'; // typeof x === string
if (map.has(x)) {
// error
map.get(x).toExponential();
// error
map.get('bar').toExponential();
} I tried to get it to work for nested |
Or an even simpler version that works for nested ifs as well: interface GuardedMap<K, V> extends Map<K, V> {
has<S extends K>(k: S): this is (K extends S ? {} : { get(k: S): V }) & this;
}
let map: GuardedMap<string, number> = new Map();
// Works for string literals!
if (map.has('foo')) {
if(map.has('bar'))
{
// works for string literals!
map.get('foo').toExponential();
map.get("bar") // ok
}
map.get('bar').toExponential(); /// error
}
declare var x: string
if (map.has(x)) {
map.get(x).toExponential() // error
map.get("x").toExponential()// error
} |
one solution i found for this was storing the map value in a variable then checking if that's undefined for before using it.
not perfect but prevents error being thrown |
Would be possible to do something like shown here? |
What about adding a getOrElse(f: () => V): V method that
|
Even with the MWE:
I feel this should be changed ? |
Wrong placing of the |
Type inference shouldn't fail for native type-guards on primitive, period. This isn't a situation with some hokey 'flexible' API. It is literally a boolean check whether a value exists or not, for an object that is part of the core language. Typescript should support language primitives defined by the ECMA standard, full stop. That should take precedence over a 'popular library'. There's no point in continuing this discussion if the answer is to use the '!' escape hatch, which by typescript's own linting standards declares that your code is no longer 'type-safe'. If we were having this conversation in any other language it would be considered a joke. Typescript has its flaws, but not supporting core language features is very, very bad. It means that we now have two competing standards: ECMA standard and the Typescript standard. If that's the game then maybe it is time to switch to another language. |
well no, flow does the exact same thing and they didn't even give it a second thought so at least there it was not a joke.
Could you please point me to the place where ECMA standards explicitly endorses the paradigm to call The way I understand it, either your map doesn't have |
some solutions
function majorityElement(nums: number[]): number[] {
const base = ~~(nums.length / 3);
const map = new Map<number, number>();
const set = new Set<number>();
for(const num of nums) {
if(!map.has(num)) {
map.set(num, 1);
} else {
map.set(num, (map.get(num) ?? 0) + 1);
}
if((map.get(num) ?? 0) > base) {
set.add(num);
}
}
return [...set];
};
interface Map<K, V> {
// get(key: K): V | undefined;
get(key: K): V | typeof key;
get(key: K): V | K;
}
function majorityElement2(nums: number[]): number[] {
const base = ~~(nums.length / 3);
const map = new Map<number, number>();
const set = new Set<number>();
for(const num of nums) {
if(!map.has(num)) {
map.set(num, 1);
} else {
map.set(num, map.get(num) + 1);
}
if(map.get(num) > base) {
set.add(num);
}
}
return [...set];
};
|
A practical workaround would be TC39 proposal-upsert. |
auto-completion for Map.get and TS support for Map are unsatisfactory. https://stackoverflow.com/questions/70723319/object-is-possibly-undefined-using-es6-map-get-right-after-map-set microsoft/TypeScript#9619
Hope I'm not missing anything. Why does the get method adds "undefined" to its return signature? Why not use the generic that was passed in as is? const safe: Map<string, string>() = new Map();
const unsafe: Map<string, string | undefined> = new Map(); |
Because const map = new Map<string, string>();
map.set('foo', 'bar');
console.log(map.get('this_key_does_NOT_exist_in_the_map'));// undefined ;) TypeScript doesn't and cannot track what you set inside your Map (esp. for dynamic content), so for practicality it just reminds you to check yourself. However, as explained in the OP, TypeScript is missing type guarding support for |
@minecrawler undefined is always a possibility even for object literals or any other typed object I may come up with. If I specify Map<string, string> its my way of controlling the get method, In case I need to, I can explicitly pass in undefined as well. Don't see the point in TS making that decision for me. |
In the context of a const obj: Record<string, number> = { a: 17 };
console.log(obj.a); // always 17
console.log(obj.b); // always undefined... OOOPS, user didn't expect undefined because you forgot to type it!!!
function sendAPI(data: number): Promise<Response> {
return fetch('http://example.com/' + data);
}
// more practical example:
sendAPI(obj[document.querySelector<HTMLInputElement>('input[type="text"].key-input')!.value]);
// might show an error on the frontend or backend side, since it's undefined instead of a number, but at a hard to debug point... In this example, TypeScript can statically verify a few things, but not everything. And with this dangerous kind of typing, which disregards A better type for
TypeScript doesn't make that decision for you, it simply implements the standard to which you need to adhere: ECMA-262. The point is that TypeScript is typing out all possibilities to make your code statically verifiable according to the standard. Since |
@eladchen Then it's somewhat controversial why
Don't know what's the sweet pot here because adding |
I'm not sure my message is clear enough.
I'm saying typescripy should let me define whether a map has undefined
value or not.
…On Fri, Sep 22, 2023, 12:26 dexx086 ***@***.***> wrote:
@eladchen <https://github.com/eladchen> Then it's somewhat controversial
why obj.b access doesn't add then undefined as well to its type, but
Map.get does.
But then undefined should be added for simple array accesses as well,
right? Like:
const arr: string[] = [];
const badValue: string = arr[0]; // Bad, casues runtime error...
Don't know what's the sweet pot here because adding undefined to simple
indexed array accesses could be very cumbersome to handle as well. But it's *not
unified* handling if Map.get() adds undefined, while a dynamic object's
property access, or even just a simple array indexed access doesn't...
—
Reply to this email directly, view it on GitHub
<#9619 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA3V64MVH3HCP6SJMINZIJ3X3VKSTANCNFSM4CJGZCVQ>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Sry, I wanted to mention @minecrawler in my post, not you. With you basically I agree, it bothers me too that we cannot control Map's behaviour. But viewing a bigger picture, it's controversial that Map is not handled the same way regarding But still, don't know what the optimal solution could be (handling Map without undefined would align to the other cases at least, but the other cases already introduce unhandled undefined cases...). |
Ok, I understand what you mean, however there's a config option for that: I didn't want to start a discussion about basics in the language, though, since they are not in scope for this request. Let's stay on topic. TypeScript isn't perfect, and if you feel like it should be improved, you can open up a new proposal to remove the |
@minecrawler You're right, with this option we can control the mentioned Maybe the solution would be then to extend |
I can understand and agree with the handling of const map = new Map<'a' | 'b', number>([
['a', 1],
['b', 2], // leaving this out would do nothing
])
const resultMap = map.get('a')
// ^? const resultMap: number | undefined
const record: Record<'a' | 'b', number> = {
a: 1,
b: 2, // leaving this out would give a ts error:
// Property 'b' is missing in type '{ a: number; }' but required
// in type 'Record<"a" | "b", number>'.ts(2741)
}
const resultRecord = record.a
// ^? const resultRecord: number While there is probably some low-level language reason for it to be this way, it certainly is confusing. Also, it makes the use of |
Can we consider to have this naive approach like this one: const mop = new Map<string, string>([
['coco', 'coucou']
]);
function check(id?: string | null): boolean {
return mop.has(id!);
}
check(undefined) // Valid => false
check(null) // Valid => false
check(1) // Invalid cause of typing. Taking advantage to use the non-null assertion operator in the body of function should be conscientious and considering to know what we're doing. Is there any edge cases that I didnt thought about it ? |
@cdskill I don't see what
Or else, it would still have |
Actually my comment is the redundant with this one above: #9619 (comment) from use-strict |
## Description Prompted by a question in Slack. * Define internal TotalMap<K, V> and TotalMapFrom<Map<K, V>> types (cf. microsoft/TypeScript#9619 ) * Add doc comments * Add typings * Rename some functions and variables to better indicate semantics * Remove an unused parameter ### Security Considerations n/a ### Scaling Considerations n/a ### Documentation Considerations Automatically provides a better IDE experience. ### Testing Considerations n/a ### Upgrade Considerations n/a
TypeScript Version: 2.0.0-beta
Code
Expected behavior:
No errors
Actual behavior:
Type 'string | undefined' is not assignable to type 'string'.
Looks like here no TypeGuards for Maps?
The text was updated successfully, but these errors were encountered: