hotwired / stimulus

A modest JavaScript framework for the HTML you already have
https://stimulus.hotwired.dev/
MIT License
12.55k stars 420 forks source link

targets and typescript #121

Closed sunnyrjuneja closed 6 years ago

sunnyrjuneja commented 6 years ago

Hi there!

Thanks for open sourcing stimulus. It's been a real joy to use so far and I'm excited to really dig deep into it.

I noticed that the default example creates a typescript compiler error.

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "output" ]

  connect() {
    this.outputTarget.textContent = 'Hello, Stimulus!' //[ts] Property 'outputTarget' does not exist on type 'default'.
  }
}

The Stimulus documentation says:

When Stimulus loads your controller class, it looks for target name strings in a static array called targets. For each target name in the array, Stimulus adds three new properties to your controller. Here, our "source" target name becomes the following properties:

  • this.sourceTarget evaluates to the first source target in your controller’s scope. If there is no source target, accessing the property throws an error.
  • this.sourceTargets evaluates to an array of all source targets in the controller’s scope.
  • this.hasSourceTarget evaluates to true if there is a source target or false if not.

Is there anyway to let Typescript know about this.sourceTarget, this.sourceTargets, and this.hasSourceTarget? I thought this might be alleviate since the library is written in Typescript.

docstun commented 6 years ago

According to https://discourse.stimulusjs.org/t/syntax-error-when-using-targets/23/8 this is not (yet?) possible, instead you’d have to declare each target explicitly.

sunnyrjuneja commented 6 years ago

Thank you @docstun! I see that someone has the same problem as me. I should have checked the forum too :). Since the maintainers are aware of the issue and there is a workaround, I'll close this issue.

Workaround for future searchers:

import { Controller } from "stimulus"

export default class extends Controller {
  nameTarget: Element
  nameTargets: Element[] 
  hasNameTarget: boolean

  static targets = [ "name" ]

  // …
}
danielcompton commented 5 years ago

I'm not sure how easy/hard this would be to do, but it looks like a similar problem to defaultProps for JSX. That was just added in TS 3.0.

NaiveCAI commented 5 years ago

FYI:

If you use @sunnyrjuneja 's sulotion, you will need this config in tsconfig file, otherwise it will raise an error: xxxx has no initializer and is not definitely assigned in the constructor.

"strictPropertyInitialization":false

sunnyrjuneja commented 5 years ago

@NaiveCAI This is my tsconfig:

{
  "compilerOptions": {
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es6", "dom"],
    "module": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5"
  },
  "exclude": [
    "**/*.spec.ts",
    "node_modules",
    "vendor",
    "public"
  ],
  "compileOnSave": false
}
sstephenson commented 5 years ago

FYI:

If you use @sunnyrjuneja 's sulotion, you will need this config in tsconfig file, otherwise it will raise an error: xxxx has no initializer and is not definitely assigned in the constructor.

"strictPropertyInitialization":false

You can use an exclamation point to bypass the strict initialization check:

class extends Controller {
  static targets = [ "name" ]

  readonly nameTarget!: Element
  readonly nameTargets!: Element[]
  readonly hasNameTargets!: boolean
}
fiznool commented 5 years ago

I've been trying to use the approach listed here but I've come across an issue.

Using syntax such as

class extends Controller {
  static targets = [ "name" ]

  readonly nameTarget!: Element
  readonly nameTargets!: Element[]
  readonly hasNameTargets!: boolean
}

This gets compiled down to:

_this.nameTarget = void 0;
_this.nameTargets = void 0;
_this.hasNameTarget = void 0;

And an error appears in the console:

TypeError: Cannot set property nameTarget of #<Controller> which has only a getter

Pausing the debugger at this point, inspecting this.nameTarget reveals that it has already been set to the correct DOM element, and stimulus is (correctly) stopping any assignment to this value.

I wonder if there is a way to tell TypeScript that we have a nameTarget property without compiling this into code that tries to initialise the property? (I do wonder if this is a babel issue rather than TypeScript though).

EDIT: on further investigation it appears that this is the behaviour of the @babel/plugin-proposal-class-properties plugin, if you do not initialise the property then it will compile down to initialise with void 0. I guess I need to find another way to make TypeScript happy 😄

Related: https://github.com/babel/babel/issues/7644

I'll open a new issue.