Skip to content

Bug: [no-unnecessary-condition] False positive Checking Lookup Types #6264

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

Closed
4 tasks done
uhyo opened this issue Dec 22, 2022 · 8 comments · Fixed by #6452
Closed
4 tasks done

Bug: [no-unnecessary-condition] False positive Checking Lookup Types #6264

uhyo opened this issue Dec 22, 2022 · 8 comments · Fixed by #6452
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@uhyo
Copy link
Contributor

uhyo commented Dec 22, 2022

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Playground Link

https://typescript-eslint.io/play/#ts=4.9.3&sourceType=module&code=KYDwDg9gTgLgBAMwK4DsDGMCWEVwObAwA8A8gEYBWANHANLACecoMwKAJgM5wDWjECOOQoA+ABQRKALiGUafBjPoMAlHADeAKDhw0OTvABuAQwA2SYHAC8cSRQDaCgLoBubXEyCxJ88DVadHShCJChcHws3HQBfdxgACygIAHc4FGBUgFEoJKgxAHIAIRISAFkAQnyVTVjNAhgxdUQICBkUJFNTOGiaACIEFt6VFyA&eslintrc=N4KABGBEBOCuA2BTAzpAXGUEKQAIBcBPABxQGNoBLY-AWhXkoDt8B6Jge1tiacTJTIAhtEK0yHJgBNK+SpPRRE0aB2iRwYAL4gtQA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA

Repro Code

export function get<Obj, Key extends keyof Obj>(obj: Obj, key: Key) {
  const value = obj[key];
  if (value) {
    return value;
  }
  throw new Error('BOOM!')
}

get({ foo: null }, "foo");

ESLint Config

module.exports = {
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module",
        "project": "./tsconfig.json"
    },
    "plugins": [
        "@typescript-eslint"
    ],
    "rules": {
        "@typescript-eslint/no-unnecessary-condition": "error"
    }
}

tsconfig

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Expected Result

No error.

Actual Result

The rule flags if (value) as error because it thinks value is always truthy.

Additional Info

The type of value is Obj[Key] here. Maybe the rule wrongly thinks that generic lookup types are always truthy.

@uhyo uhyo added bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Dec 22, 2022
@bradzacher bradzacher changed the title Bug: [no-unnecessary-condition] False Negative Checking Lookup Types Bug: [no-unnecessary-condition] False positive Checking Lookup Types Dec 22, 2022
@bradzacher
Copy link
Member

isTypeFlagSet(part, ts.TypeFlags.TypeParameter),

The lint rule specifically ignores types marked as "type parameters", which Obj is.
Obj[Key], OTOH is not flagged as a "type parameter" by TS.

It is flagged as an index access type, which is part of the "type variable" flag - so maybe we need to update our code to use that flag instead? Unsure of the ramifications.

@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue and removed triage Waiting for team members to take a look labels Dec 22, 2022
@ehoogeveen-medweb
Copy link

I ran into this issue as well with variants of the following code:

function getElem(dict: Record<string, { foo: string }>, key: string) {
    if (dict[key]) { // Unnecessary conditional, value is always truthy. 2:9 - 2:18
        return dict[key].foo;
    } else {
        return '';
    }
}

Playground link

Essentially the rule is very overconfident when it comes to index signatures, which can never really be guaranteed. It's unfortunate because the only workaround I found was to wrap the type in Partial<>, which then adds undefined to the type even if you've checked that the property exists:

function getElem(dict: Partial<Record<string, { foo: string }>>, key: string) {
    if (dict[key]) {
        return dict[key].foo; // Error 2532: Object is possibly 'undefined'. 3:16 - 3:25
    } else {
        return '';
    }
}

function getElem2(dict: Partial<Record<string, { foo: string }>>, key: string) {
    const elem = dict[key];
    if (elem) {
        return elem.foo; // OK
    } else {
        return '';
    }
}

Playground link

As an aside, setting noUncheckedIndexedAccess: true in the compiler options has essentially the same effect: Playground link

@Josh-Cena
Copy link
Member

Josh-Cena commented Feb 10, 2023

@ehoogeveen-medweb Narrowing doesn't work with expression indexers. You can only narrow static variables, which is why your revised version works. For this, see microsoft/TypeScript#10530 and its many duplicates. The rule only uses whatever TS tells it as the expression's type—see #2883. This issue is specifically about generically typed variables.

@ehoogeveen-medweb
Copy link

@Josh-Cena I know narrowing doesn't work, but the problem is that this rule claims that dict[key] is always truthy, which is not true for index signatures.

@uhyo
Copy link
Contributor Author

uhyo commented Feb 11, 2023

@ehoogeveen-medweb In your example dict[key] is considered always truthy by TypeScript, so TypeScript is to blame for this behavior, not this rule. As you mentioned, the noUncheckedIndexedAccess compiler option is there exactly for fixing this behavior. To get what you want, you should keep that option on.

Just in case, I included test cases for your example in the PR I just created.

@ehoogeveen-medweb
Copy link

ehoogeveen-medweb commented Feb 11, 2023

@uhyo Right, it's a limitation of TypeScript that noUncheckedIndexedAccess attempts to fix, but unfortunately TypeScript isn't quite expressive enough to make noUncheckedIndexedAccess workable.

For example there's no general way to add the information "the value contained in variable A is a key of type T" to type T (it works for literal types but not for anything wider or generic), so something as simple as if (key in obj) val = obj[key]; will often generate an error under noUncheckedIndexedAccess.

Similarly TypeScript does not have a way to dynamically track or represent the minimum size of arrays, so things like if (array.length) val = array[0]; will often generate an error under noUncheckedIndexedAccess.

You can work around these things to some extent using helper functions, but on some level those functions have to lie about the type and allow unchecked access within a given scope (for example you can make a helper function like arrayNotEmpty that turns the type into [T, ...T[]] but then the type may be wrong once you pop or shift). So you end up with code that looks weird because of all the indirect access and you end up losing some of the safety of the option anyway.

I think on a conceptual level index signature properties should be considered as optional properties (at least with exactOptionalPropertyTypes enabled, to distinguish between properties that are missing and properties that may have the value undefined), but it doesn't seem like this is something the language itself supports.

For this rule I think your PR is exactly what is needed: We can't know for sure that the condition is unnecessary for an indexed access type, so those shouldn't trigger an error.

In general I think it would be great if there was a rule that... checked for unchecked indexed access on objects. But if TypeScript itself struggles to do that then it's probably an unreasonable thing to request out of typescript-eslint :)

@Josh-Cena
Copy link
Member

@ehoogeveen-medweb This issue is not about your case. It's about the case where the expression is of a generic type that provably may include undefined. Your case is where the expression is of a concrete type that does not include undefined. This part has been responded to in #2883. noUncheckedIndexedAccess should absolutely be an option to consider turning on; we do not plan to wrap additional logic beyond what TS tells our rule.

@ehoogeveen-medweb
Copy link

@Josh-Cena I understand, and I'm glad that this issue has been fixed. I was just trying to explain why enabling noUncheckedIndexedAccess was not a good workaround for avoiding these warnings and why I suspect it never will be.

I may attempt to create a rule that gives the same protection as noUncheckedIndexedAccess as I'm beginning to suspect that it's easier as a linting rule than as a compiler option, but that's certainly beyond the scope of this issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
4 participants