Babel-eslint: Use # prefix on private class field with eslint-plugin-flowtype cause "Cannot read property 'type' of undefined"

Created on 23 Sep 2018  路  17Comments  路  Source: babel/babel-eslint

My plugins and parser versions:

"babel-eslint": "^9.0.0",
"eslint": "^5.5.0",
"eslint-plugin-babel": "^5.2.0",
"eslint-plugin-flowtype": "^2.50.1",
"flow-bin": "^0.81.0",

My eslint configs:

    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:flowtype/recommended"
    ],
    "parser": "babel-eslint",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": ["react", "babel", "flowtype"],

I got the problem when write this:

...
class Popup extends React.Component {
    #previousRange: ?Range = null;
...
}
Class Popup 
TypeError: Cannot read property 'type' of undefined

TypeError: Cannot read property 'type' of undefined
    at isForInRef (..\node_modules\eslint\lib\rules\no-unused-vars.js:410:24)
    at variable.references.some.ref (..\node_modules\eslint\lib\rules\no-unused-vars.js:447:21)
    at Array.some (<anonymous>)
    at isUsedVariable (..\node_modules\eslint\lib\rules\no-unused-vars.js:446:40)
    at collectUnusedVariables (..\node_modules\eslint\lib\rules\no-unused-vars.js:569:26)
    at collectUnusedVariables (..\node_modules\eslint\lib\rules\no-unused-vars.js:576:17)
    at Program:exit (..\node_modules\eslint\lib\rules\no-unused-vars.js:621:36)
    at listeners.(anonymous function).forEach.listener (..\node_modules\eslint\lib\util\safe-emitter.js:47:58)
    at Array.forEach (<anonymous>)
    at Object.emit (..\node_modules\eslint\lib\util\safe-emitter.js:47:38)
    at NodeEventGenerator.applySelector (..\node_modules\eslint\lib\util\node-event-generator.js:251:26)
    at NodeEventGenerator.applySelectors (..\node_modules\eslint\lib\util\node-event-generator.js:280:22)
    at NodeEventGenerator.leaveNode (..\node_modules\eslint\lib\util\node-event-generator.js:303:14)
    at CodePathAnalyzer.leaveNode (..\node_modules\eslint\lib\code-path-analysis\code-path-analyzer.js:630:23)
    at CodePathAnalyzer.overrideLeaveNode (..\node_modules\eslint-plugin-node\lib\rules\process-exit-as-throw.js:127:27)
    at Traverser.leave [as _leave] (..\node_modules\eslint\lib\linter.js:1000:32)
Process finished with exit code -1

Most helpful comment

This exact issue still persists for me even if I crank all eslint & bable dependencies to their latest version. What's more, this doesn't always fail. For example in my case

This fails

  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: Array<number>; // only this specific private field causes an error.

This passes

  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: string; // note: this changed

Adding this totally unrelated type makes it work

type TotallyUnrelatedType = Array<number>;
export class Foo {
  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: Array<number>;
}

All 17 comments

Do you have the right babel plugin? ["@babel/plugin-proposal-class-properties", { "loose": false }] if you're on Babel 7. See the README here (EDIT: Or just jump to the blog post) for more info, but basically, you have to include the plugins individually now.

Yes i use babel 7:

"@babel/core": "^7.1.2",
"@babel/plugin-proposal-class-properties": "^7.1.0",        
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-transform-dotall-regex": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",

This is my config:

...
options: {
    presets: [
        '@babel/preset-env',
        '@babel/preset-react',
        '@babel/preset-flow'
    ],
    plugins: [
        '@babel/plugin-proposal-object-rest-spread',
        '@babel/plugin-proposal-unicode-property-regex',
        '@babel/plugin-transform-dotall-regex',
        'babel-plugin-styled-components',
        ['@babel/plugin-proposal-class-properties', { loose: false }],
        '@babel/plugin-proposal-json-strings',
        '@babel/plugin-syntax-dynamic-import',
        '@babel/plugin-syntax-import-meta'
    ]
},
...

I still have this error when ever i type prefix# when declare a member in class. I think it must be an issue

Does it output correctly at least, or is just when you're linting? If you're doing a babel.config.js file, you don't need to wrap them in the options key. You can just

module.exports = (api) => {
  const presets = [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-flow'
  ];
  const plugins = [
    '@babel/plugin-proposal-object-rest-spread',
    '@babel/plugin-proposal-unicode-property-regex',
    '@babel/plugin-transform-dotall-regex',
    'babel-plugin-styled-components',
    ['@babel/plugin-proposal-class-properties', { loose: false }],
    '@babel/plugin-proposal-json-strings',
    '@babel/plugin-syntax-dynamic-import',
    '@babel/plugin-syntax-import-meta',
    api.env('test') ? 'babel-plugin-istanbul' : '' // if you use special environmental plugins/presets, here's how to do it
  ];
  return { plugins: plugins, presets: presets }; // or { plugins, presets }, as is usually preferred
};

I'm not trying to say that it _isn't_ a bug, just that babel changed the way it works vastly, so if you're taking advantae of the new configuration features, there's a _chance_ that the problem could be with babel as well/instead.

yes i just got when linting, babel is fine

@philong6297 did you figure out the linting issue?

Same issue here when using babel-eslint.

Digging deeper, in no-unused-vars.js it would appear target is undefined:

function isForInRef(ref) {
            let target = ref.identifier.parent;


            // "for (var ...) { return; }"
            if (target.type === "VariableDeclarator") {
                target = target.parent.parent;
            }

Seems in my case (check your yarn.lock/package-lock.json) there were conflicting versions of babel-eslint in play. Adding a resolution to package.json seems to fix for me:

"resolutions": {
    "babel-eslint": "^10.0.1"
  }

I have created a demo of an issue:

https://github.com/SleepWalker/bug-eslint-no-unused-vars-private-fields

Adding a resolution to package.json seems to fix for me

@jamiehill In my case that haven't fix anything :( the error still persists

Seems like the reason is that espree has no support for private properties

Seems like the reason is that espree has no support for private properties

But, isn't that why we use babel-eslint? To make eslint work properly with features espree does not support yet?

Ha, it is about flow types in private properties.

Minimal reproducible test:

it("typed private properties", () => {
  verifyAndAssertMessages(
    `
        type Something = { };

        class C {
          #obj: Something;
        }

        console.log(new C());
    `,
    { "no-unused-vars": 1 }
  );
});

Probably we need to reimplement the no-unused-vars rule in eslint-plugin-babel.

This might be another relevant note,

Private fields and variables was linting fine for me _before_ i added 'plugin:flowtype/recommended' to my extends array in my .eslintrc.js

extends: [
        'eslint:recommended',
        'plugin:prettier/recommended',
        'plugin:flowtype/recommended'
    ]

This exact issue still persists for me even if I crank all eslint & bable dependencies to their latest version. What's more, this doesn't always fail. For example in my case

This fails

  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: Array<number>; // only this specific private field causes an error.

This passes

  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: string; // note: this changed

Adding this totally unrelated type makes it work

type TotallyUnrelatedType = Array<number>;
export class Foo {
  #publicDeriverId: number;
  #bip44Wallet: IBip44Wallet;
  #derivationId: number;
  #pathToPublic: Array<number>;
}

This still occurs for me as well specifically when using any type alias. Primitives work as largely noted by @SebastienGllmt

Works

class Something {
  #config: string;
}

Fails

class Something {
  #config: Array<string>;
}
type Example = string;

class Something {
  #config: Example
}

This definitely seems to just be a parsing issue. I did see some typescript parser issues which were related that they had to fix so it makes sense.

For now the past year+ we have had to just use WeakMap to get private properties which isn't ideal but what can ya do?

type Example = {| one: string |};

const CONFIG: WeakMap<Sample, Example> = new WeakMap();

class Sample {
  constructor() {
    CONFIG.set(this, { one: 'hi' });
  }
}

Hi! I just experienced this bug at work today :/ However, I found a different workaround that I didn't see listed here, so I'll share it, in case it can be helpful to anyone!

Basically, instead of typing the property using proper Flow annotations, I tried to cast a dummy value, and it prevented eslint from crashing on the 'type' of undefined error:

class MinimalExample {
  // Toggle the comments between these two properties, the second line works!
  #privateProp: Array<number> = [] // Breaks eslint :(
  // #privateProp = ([]: Array<number>) // This works :) !
}

One downside is that it does not prevent the type to be "unioned" with other types in your methods if you happen to change it, say, from an array of numbers to a number proper. It will keep track of the union, at least, but I'd rather get the error at the reassignment (which works fine without the workaround, but then eslint is broken instead) than when I try using it later.

However, it does prevent inserting improper data inside the array for this example, so you couldn't push a string into it by accident, at least!

You can see that behavior if you add this to the class and uncomment one method at a time:

// Only uncomment one at a time, since the reassignment to a number breaks
// the addString below, where #privateProp will be treated as
// `Array<number> | number`.

// setNumber() {
//   this.#privateProp = 42 // Shouldn't work :(
//   // But at least, you'll catch it when you try to read from the array later.
// }

// addString() {
//   this.#privateProp.push('42') // But this does trigger an error properly :)
// }

So I guess if you can tolerate a bit of fuzziness in the type safety in exchange for eslint working properly, this might be another good workaround!


However, this might not be necessary, as playing around with @SebastienGllmt's suggestion using @bradennapier's example made me see that adding an extra type alias seems to fix the problem, at least it did in our code where we encountered the issue. Here's the example from above:

// Fails indeed :(
type Example = string;

class Something {
  #config: Example
}

So trying the "totally unrelated type" solution from above, I realized that creating a dummy alias for another alias... works? Just make sure not to use the dummy alias, but the original alias after having declared the dummy alias, strangely enough.

// Works :O !
type Example = string;
type WhyDoesThisWork = Example;

class Something {
  #config: Example
  // #config2: WhyDoesThisWork // This does not work actually, only the original alias works...
}

So, I guess there's two different workarounds for the problem, at least, for others that will get it.

Thank you for the issue. Now that @babel/eslint-parser has been released, we are making this repository read-only. If this is a change you would still like to advocate for, please reopen this in the babel/babel monorepo.

Was this page helpful?
0 / 5 - 0 ratings