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
[no-unnecessary-condition] ignore array index return #1798
Comments
It would not really be possible, no. From the POV of typescript, a string is a string. Doesn't matter where it came from. Which means in order to detect this, the rule would have to do control-flow analysis of a variable to determine where the value in any variable has come from. There's but one case that's "easy" to solve, and that is a const variable whose value was directly assigned from an array index: const bar = foo[5];
if (bar) {} Happy to accept a PR if you want to support this case, otherwise I'd say that it's not possible to do. const foo = ['a', 'b', 'c'];
const bar = foo[5];
if (bar) {} // "relatively" easy to detect, as there has been exactly one assignment.
let bam = foo[5];
bam = 'a';
if (bam) {} // hard to tell that if the value is an array
if (someCondition) { bam = 'a'; }
if (bam) {} // impossible to know if the value came from an array
const buz = foo[5];
const bang = buz;
if (bang) {} // really, really, really hard to know if the value came from an array
test(bar);
function test(baz: string) {
if (baz) {} // impossible to detect
}
|
Thanks @bradzacher, yet another amazing piece of feedback, very helpful :) In the examples above, this is also an acceptable alternative and satisfies the linter: const foo = ['a', 'b', 'c']; // type: string[]
// Passes the rule
if (5 in foo) {
console.log(foo[5]);
} (Or course it's a little silly like this but imagine a dynamic index) As we go through all our projects one by one, we'll see if other cases arise. Should the format described above come up too often, I'll see if I can look at improving the linter. |
Following-up on this, it would also be great to be able to ignore In our own code, we have something of the sort: if (!client.getResolvers().Foo?.id) {
client.addResolvers({
Foo: {
id: /* ... */,
},
});
}
[key: string]: {
[field: string]: { /* ... */ };
}; In other parts of the code, we have been able to rewrite the value of the record as Because I think having one or multiple options to ignore things like this, lookups that can be undefined even though TS typing indicates it is not, would make things safer, as following the linter recommendation breaks the code here. What do you think? |
The problem is that that that option would add a lot of unsafety in the rule, as you'll no longer get warned about records within your control. A workaround is to inline the call and apply a custom type: declare const client: {
getResolvers(): Record<string, Record<string, number>>;
addResolvers(resolver: Record<string, Record<string, number>>): void;
};
type SafeResolversRecord = Record<string, Record<string, number | undefined> | undefined>;
const resolvers: SafeResolversRecord = client.getResolvers();
if (resolvers.Foo?.id) {
client.addResolvers({
Foo: {
id: 1,
},
});
} depending on how the module defines its types, you could even augment this type into the module itself to make it easier to consume: import "foo";
declare module "foo" {
export type SafeResolversRecord = Record<string, Record<string, number | undefined> | undefined>;
}
////////
import { SafeResolversRecord } from 'foo'; |
That's really great advice, thank you @bradzacher! |
microsoft/TypeScript#13778 would fix this, right? |
Yup! It would definitely fix this. Because of #1534, we handle the trivial to detect case of We can use scope analysis to handle this case: const x = arr[0];
if (x) {} // detectable Any other cases mentioned in #1798 (comment) get very fuzzy and likely to false positive/negative. |
In addition to array types, this also causes problems for Map index if I have a type like:
The index into the map can certainly be undefined - but the no-unnecessary-condition flags it - this is not an array index it is an index into a string map. |
@jmicco Yeah, that's a well-known limitation as well. Unlike the array case, that one has a pretty good workaround, though. It's better to declare your map in a way that indicates that keys might not exist: interface ITargetMap {
[target: string]: ITargetEntry | undefined
}
// Or `type ITargetMap = Partial<Record<string, ITargetEntry>>` for short |
Hi @Retsam, I thought I tried that and it did not work - my apologies, that
fix did prevent the complaint, although it seems like the type imputation
for such a hash needs to generate the type as "x | undefined", but I
understand that is the problem for the typescript people and not for this
rule.
…On Mon, Jul 27, 2020 at 3:44 PM Retsam ***@***.***> wrote:
@jmicco <https://github.com/jmicco> Yeah, that's a well-known limitation
as well. Unlike the array case, that one has a pretty good workaround,
though. It's better to declare your map in a way that indicates that keys
might not exist:
interface ITargetMap {
[target: string]: ITargetEntry | undefined}// Or `type ITargetMap = Partial<Record<string, ITargetEntry>>` for short
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1798 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABSTB6AVM7JI2CXTRHIXBXLR5X7LFANCNFSM4LTVENYA>
.
|
As a side note, |
The best solution we could build in here would be to add some scope analysis to the rule to help it understand where a value came from. That would let us automatically handle some more cases that we currently ignore when it's inline in the condition. Going forward I would definitely agree that everyone should just be using
This rant is a bit off topic so I've put it behind a spoiler block - but I just wanted to highlight to people why having "provably unnecessary" checks in your codebase isn't a problem as a motivating case for why you should migrate codebases to
|
I really enjoy this rule because it makes tidying up after a refactor a lot more effective, but the situation at the moment is a little tricky to manage.
Could something like |
theoretically - yes - it could be implemented. const x: number[] = [];
if (x[0]) {
x[0].length; // is "safe" due to the condition
} Which is really the most complicated part of it. You'd ultimately just be rebuilding a chunk of TS |
I'd be interested in updating this rule to cover the most trivial additional case, as I've run into this issue in my own codebase:
However, I've never worked on linter rules before, and I'm not sure where to start. It sounds like this is detectable via "scope analysis" - could anyone offer advice on how I might be able to learn how to do this? |
I'm having trouble with false negatives and this is the only issue I could find on this topic. I can open a separate one if needed. Copied here for convenience: declare const actionData: {
email: string | string[];
password: string | string[];
} | undefined
export const result = actionData?.email?.[0];
// ^^ -> ts-eslint thinks this is unnecessary I have TS |
@frontsideair you can access string characters using array index notation. So your optional chain does nothing - the report is correct. You need to write stricter code using either But either way - according to your runtime error your types are simply incorrect. We can only act on the types you provide - |
First of all, thanks for taking the time to look into this. However I would never expect ts-eslint to work with the types I didn't provide, but the type I provided is not Also I removed the I would expect this rule to not report the last optional chain as unnecessary. |
@frontsideair that's not how optional chaining works! Based on your types Put another way, it's equivalent to this code: // actionData?.email[0] is the same as
let result = undefined;
if (actionData != null) {
result = actionData.email[0];
} Note how the Whilst the result of To further illustrate, if you were to write your unnecessary code (based on your types), you would end up with this equivalent code: // actionData?.email?.[0] is the same as
let result = undefined;
if (actionData != null) {
if (actionData.email != null) {
result = actionData.email[0];
}
} See now how that second optional chain is truly unnecessary based on your types? |
Yes, you are absolutely correct. I was confused because the types I'm actually working were coming from a library and it seems to be wrong. I simplified them as they were shown and got it wrong by extension. I'll try to open an issue with the library itself, thanks for clarifying! |
Consider the following snippet:
Thanks to @Retsam's work on #1534, the first example passes the linter, however as the author mentions in their PR, it doesn't handle less trivial cases, such as storing the value in a variable (which is very common when you want to use the value being tested in the body of the
if
).I realize that supporting this (and other advanced) cases might be a lift. In the meantime, or alternatively, any thoughts on an option to entirely ignore arrays? In the current state, I don't believe there is a way to make the second example above pass the linter without disabling the rule or getting rid of the variable.
I believe such option was mentioned in #1544, for reference.
Versions
@typescript-eslint/eslint-plugin
2.24.0
@typescript-eslint/parser
2.24.0
TypeScript
3.8.3
ESLint
6.8.0
node
12.14.0
npm
6.13.4
The text was updated successfully, but these errors were encountered: