What JsDoc can do for your Tabris.js Project
Tabris.js provides out-of-the-box autocompletion in Visual Studio Code and Webstorm (and some other IDEs) via the included type declarations file. While based on TypeScript syntax, these declarations work perfectly fine in your pure “Vanilla” JavaScript project. Take this mini-app:
1 2 3 4 5 6 7 |
const {Button, contentView} = require('tabris'); new Button({ centerX: true, top: 100, text: 'Tap me!' }).onSelect(ev => ev.target.text = 'Thanks!!') .appendTo(contentView); |
In the case of the “select” event listener, the IDE “knows” what properties the “ev” object has, including the type of the “target”. That is why it can give you a list of suggestions while you type:
This works fine as long as your code is interacting directly with the Tabris API. However, what happens when we extract the listener into its own named function?
1 2 3 4 5 6 7 8 9 10 11 |
const {Button, contentView} = require('tabris'); new Button({ centerX: true, top: 100, text: 'Tap me!' }).onSelect(handleSelect) .appendTo(contentView); function handleSelect(ev) { ev.target.text = 'Thanks!!'; } |
Well, that’s not so great. Within Tabris.js application code this is probably the most common scenario where the IDE is no longer able to infer what type a variable has. That is where JsDoc can help, specifically the “@param” tag:
1 2 3 4 5 6 7 |
/** * @param {object} ev * @param {tabris.Button} ev.target */ function handleSelect(ev) { ev.target.text = 'Thanks!!'; } |
This indicates to the IDE that “ev” will be an object and that it will have a property “target” of the type “tabris.Button”. Let’s see how that works out:
Neat!
Unfortunately, this is where the journey ends for WebStorm. While it “understands” TypeScript declarations to some degree, it currently does not fully support them within JsDoc tags as Visual Studio Code does. This isn’t surprising, this is JsDoc after all – the two formats were never meant to be used together. Still, it turns out that they make a great team anyway, and VS Code takes full advantage of this. Using this knowledge, we can “explain” to the IDE, not just that “ev” is an object but a Tabris “EventObject” dispatched by a button:
1 2 3 4 5 6 |
/** * @param {tabris.EventObject<tabris.Button>} ev */ function handleSelect(ev) { ev.target.text = 'Thanks!!'; } |
Now other general event properties are known as well:
There are roughly three kinds of event objects in Tabris.
- Generic “tabris.EventObject” instances. You can always use these if you don’t need to be more specific.
- Change events, which all have a “value” property.
- Widget-specific events that provide additional information. They all have unique names of the pattern “tabris.
Event”.
We already covered how to handle the first kind. Let’s revise the app to have examples for the other two:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const {ToggleButton, contentView} = require('tabris'); new ToggleButton({ centerX: true, top: 100, text: 'Off' }).onSelect(handleSelect) .onTextChanged(textChanged) .appendTo(contentView); function handleSelect(ev) { ev.target.text = ev.checked ? 'On' : 'Off' } function textChanged(ev) { console.log(`The value of "text" is now "${ev.value}"`) } |
The “select” event is specific to the toggle button and therefore has a unique name without the need for a type parameter (the part in angle brackets):
1 2 3 4 5 6 |
/** * @param {tabris.ToggleButtonSelectEvent} ev */ function handleSelect(ev) { ev.target.text = ev.checked ? 'On' : 'Off' } |
The change event, on the other hand, requires two type parameter, the widget that dispatches the event and the name of the property that changes:
1 2 3 4 5 6 |
/** * @param {tabris.PropertyChangedEvent<tabris.ToggleButton, 'text'>} ev */ function textChanged(ev) { console.log(`The value of "text" is now "${ev.value}"`) } |
Now the type of “value” is clear:
However, auto-completion isn’t all we can achieve this way. Let’s say for a moment you accidentally mixed up which listener to register for which event:
1 |
.onTextChanged(handleSelect) |
There is a good chance you won’t notice this until you eventually get some weird behavior at runtime that you’ll have to trace back to this line. That is where VS Code can really save your skin:
Go to the project root directory and create a file “jsconfig.json”, or open it should it already exist. (A “tsconfig.json” with the “allowJs” compiler option set to “true” works as well.) Add the following:
1 2 3 4 5 6 7 8 |
{ "compilerOptions": { "module": "commonjs", "target": "es2017", "checkJs": true }, "exclude": ["node_modules"] } |
The line that’s important here is checkJs": true
. As an alternative, you can also open .vscode/settings.json
(if it exists) and add the following:
"javascript.implicitProjectConfig.checkJs": true
It may take a bit until the changes have an effect; when in doubt reopen the project. Now the editor should be able to discover your mistake:
As always, error messages generated by the TypeScript tooling/compiler are pretty verbose, but you should quickly be able to get the gist of it – that listener isn’t compatible with the event you are using it for.
Of course, you will only reliably be able to catch these mistakes if you use JsDoc on all your functions, properties, and variables. You can force yourself to do this by adding another compiler option:
"noImplicitAny": true
Now you will get an error whenever the IDE doesn’t know that type a variable is:
It should be obvious that if you use this on an existing project, you will likely get a lot of such errors. In my opinion, there is no reason not to live with these for a time; assuming you are a good boy scout, you will be able to fix them all eventually. That said, if you are really going to use the “noImplicitAny” option, you may as well consider switching to TypeScript entirely since there is little difference between that and a JavaScript project that uses JsDoc everywhere.
Next time I will have a look at using JsDoc for custom component authoring. See you then!
Feedback is welcome!
Want to join the discussion?Feel free to contribute!