stimulus: Webpacker 5.1 breaks recommended TypeScript controller pattern

See: https://github.com/rails/webpacker/issues/2558

Webpacker 5.1 changed the loader for TypeScript, which changed code compilation behavior, which breaks class properties.

So the controller declaration:

I have a simple Stimulus controller, defined to start as follows:

import { Controller } from "stimulus"

export default class extends Controller {
  static classes = ["hidden"]
  static targets = ["filterInput", "concert"]

  hiddenClass: string
  concertTargets: HTMLElement[]
  filterInputTarget: HTMLInputElement

  // and so on

Now breaks with a TypeError: Cannot set property hiddenClass of #<extended> which has only a getter.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 15 (4 by maintainers)

Most upvoted comments

To fix this I removed ['@babel/preset-typescript', { 'allExtensions': true, 'isTSX': true }] from babel.config.js and added

["@babel/plugin-transform-typescript", { 'allExtensions': true, 'isTSX': true, 'allowDeclareFields': true }], to the top of my plugins entry (anywhere above @babel/plugin-proposal-class-properties should work).

This will also allow the use of declare declare containerTarget: HTMLDialogElement; which will future proof your controllers should TS ever align more closely with the ECMA spec.

Ran into this issue after upgrading to Stimulus 2.0, running a Rails app with Webpacker 5.2.1. It complains when you use targets in a Stimulus controller built in TypeScript. @fleck’s suggestion fixed it.

In babel.config.js:

    plugins: [
      "babel-plugin-macros",
      "@babel/plugin-syntax-dynamic-import",
      isTestEnv && "babel-plugin-dynamic-import-node",
      "@babel/plugin-transform-destructuring",
      // added this
      [
        "@babel/plugin-transform-typescript",
        { allExtensions: true, isTSX: true, allowDeclareFields: true },
      ],

In the TypeScript controller:

export default class extends Controller {
  static targets = ["addItem", "template"];

  // TypeScript with Stimulus 2.0 must declare properties
  // https://github.com/stimulusjs/stimulus/issues/303#issuecomment-653630360
  declare templateTarget: HTMLElement;
  declare addItemTarget: HTMLElement;

@noelrappin @fleck

I think I’ve nailed down the error you were getting here:

SyntaxError: ./app/webpack/controllers/eoi_controller.ts: TypeScript 'declare' fields must first be transformed by @babel/plugin-transform-typescript.
If you have already enabled that plugin (or '@babel/preset-typescript'), make sure that it runs before any plugin related to additional class features:
 - @babel/plugin-proposal-class-properties
 - @babel/plugin-proposal-private-methods
 - @babel/plugin-proposal-decorators
  2 | 
  3 | export default class extends Controller {
> 4 |   declare step1Target: Element;

and why the plugin solution works:

Somewhat to my surprise, this works for me if I switch to the plugin, but declare it as ["@babel/plugin-transform-typescript", { 'allExtensions': true, 'isTSX': true, 'allowDeclareFields': false }] -- not sure why, because as far as I can tell, false is the default. Nevertheless, if I do what you have, I get the same answer, and if I do what I have it works. ???

It’s likely webpacker is adding @babel/plugin-proposal-class-properties as a plugin. Plugins are run before presets which leads to the error you’re seeing.

Switching to the @babel/plugin-transform-typescript which is also included in @babel/preset-typescript means it is run first, removing the declare keyword (and probably stripping the class field) allowing @babel/plugin-proposal-class-properties to work correctly.

if webpacker is using @babel/preset-typescript then it’s likely that @babel/plugin-transform-typescript is being called twice which is potentially slowing down the build.

Babel 7.14 enables class fields & private methods by default in @babel/preset-env so adding the @babel/plugin-proposal-class-properties plugin is not required if you’re using Babel 7.14 or greater and @babel/preset-env

I mean, if it works, I guess I’ll switch to it, but I’m not looking forward to explaining it in the book.

The Babel team seems unlikely to change this behavior, so unless TypeScript changes for some reason, I think we’re stuck with trying to make this work better on the Stimulus side.

@javan It seems like it would be. Unfortunately, it seems like that feature doesn’t change the runtime behaviour and still causes the error mentioned.

It seems to me like declare fields might be the right approach. I.e.

export default class extends Controller {
  static classes = ["hidden"]
 
  declare hiddenClass: string
 
  // …
}

However, I don’t seem to be able to get the allowDeclareFields @babel/preset-typescript option to work. I get the following error in the browser console:

certificates_controller.ts:45 Uncaught Error: Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: ./app/webpack/controllers/eoi_controller.ts: TypeScript 'declare' fields must first be transformed by @babel/plugin-transform-typescript.
If you have already enabled that plugin (or '@babel/preset-typescript'), make sure that it runs before any plugin related to additional class features:
 - @babel/plugin-proposal-class-properties
 - @babel/plugin-proposal-private-methods
 - @babel/plugin-proposal-decorators
  2 | 
  3 | export default class extends Controller {
> 4 |   declare step1Target: Element;