Velo by Wix: Type safety your code with JSDoc
Alexander Zaytsev
Posted on February 1, 2022
Built-in code checker, JSDoc annotations, and TypeScript compiler in Velo
From time to time, I can see in the big Velo projects how a part of the page code moves to the public files. In most, it's the projects with a few hundred/thousand lines of code per page. I understand why developers do it. Also, sometimes we want to reuse some part of the code for a few site pages. It looks like a good idea to move repeated code to a public file, and reuse it.
The main problem with this pattern is that doesn't work autocomplete and ID validation of $w()
selectors in the public files. For example, we want to move a button handler to the public file. And init it on the page code.
public/initPage.js
// Filename: public/initPage.js
export const initPage = () => {
const button = $w('#button1');
button.onClick(() => { /* ... */ });
}
Page code
import { initPage } from 'public/initPage.js';
$w.onReady(() => {
// Init page code from the public file.
initPage();
});
In the public files, we can see a missing type inference. There don't work hints for $w()
selector and don't work page elements autocomplete.
public/initPage.js
// Filename: public/initPage.js
export const initPage = () => {
// 1. Autocomplete for ID suggestions doesn't work
// 2. The checking of an element ID doesn't work.
// 3. If the element with this ID doesn't exist on the page
// we don't have any error messages in editor.
// 4. button mark as `any` type
const button = $w('#button1');
// 1. Autocomplete for button's method/properties doesn't work
// 2. Type checking doesn't work.
button.onClick(() => { /* ... */ });
}
For me, it's the main reason for don't use this pattern. The element could be removed or renamed at any time, and we don't have any editor hints, errors, or warnings to catch it. We could get a runtime error and, we should debug it with console or site logs.
However, this pattern is very commonly used. So, let's do it a little bit safer.
Why does it happen?
Firstly, the public files don't design for using the $w()
selector. Velo code checker doesn't know how we plan to use a public file. Because we can import public files to any files on any pages, also we can import a public file to the backend files, other public files, or custom web-component code.
How Velo autocomplete works?
Velo uses a TypeScript compiler for autocomplete and code validations. Each page code file has built-in types of all elements on the current page.
Velo currently uses a TypeScript compiler for autocomplete and code validations
Page element types are generated automatically, when we add/remove any element on the page, Velo adds/removes a property for this target element in PageElementsMap
type. The PageElementsMap
type is unique on each page. So, each page code file has its own map of elements for autocompletion.
We to able to use this type with JSDoc types annotation. For example, we can use a TypeScript JSDoc syntax to describe types.
Page code
/**
* @template {keyof PageElementsMap} T
*
* @param {T} selector
* @param {$w.EventHandler} eventHandler
* @returns {PageElementsMap[T]}
*/
const clickHandler = (selector, eventHandler) => {
const element = $w(selector);
element.onClick(eventHandler);
return element;
}
// You can see this function has type checking for arguments and return value
clickHandler('#button1', (event) => {
console.log(event);
});
If you try to use the code snippet above on your page code file, you can see that it has all type checking and autocomplete for arguments and a returned value. It's amazing, but we still can't use it on the public files, because the PageElementsMap
type available only on the page code files.
How can we use a JSDoc on public files?
As we can see above, the autocomplete of the $w()
selector doesn't work on the public files because TypeScript doesn't know about the context of the public file use. We can import public files anywhere in the code. So, we should describe the types.
Variable annotations with @type
tag
Let's start with the simple use case. We can add variable annotations with the @type
tag.
Syntax
JSDoc it's a block comment that starts with
/**
and ends with*/
. Inside block comment, we use a tag keyword that starts with@
symbol after tag keyword inside curly braces{}
we put a type.
/** @tag {type} */
Velo provides autocomplete and syntax validation for JSDoc annotations. Just try to write the next snippet code in Velo editor without copy-pasting.
Velo: simple example of @type
tag
/** @type {$w.Button} */
const button = $w('#button1');
$w.Button
it's a built-in type. Velo has built-in types for all page elements. You can find it here: Wix element types
The main benefits of the element types, we can use it on the public files. In the simple use case, we add the type annotations to all elements that we start to use in a public file.
public/initPage.js
// Filename: public/initPage.js
export function initPage() {
/** @type {$w.Button} */
const button = $w('#button1');
/** @type {$w.TextInput} */
const input = $w('#input1');
/** @type {$w.Text} */
const text = $w('#text1');
// your code goes here ...
}
Now, TypeScript understands what kind of elements we want to use. But TS still can check it.
Here, we just say to TypeScript - "Hey TS, I know it is the button. Just trust me and work with this element as the button".
We solve a problem with autocomplete suggestions for elements methods and properties in the public files. But we don't solve the issue when an element is removed or renamed from the page. TypeScript compiler can check $w()
selectors only on the page code files.
Arguments annotation with @param
tag
So, if we want to get autocomplete for elements and validation for $w()
selectors, we should pass the elements explicitly from the page code to the public file as function arguments.
@param
tag uses the same type syntax as @type
, but adds a parameter name.
Syntax: JSDoc function arguments
/**
* @param {type} name
*/
Let's update initPage()
function for two arguments:
public/initPage.js
// Filename: public/initPage.js
/**
* @param {$w.Button} button
* @param {$w.TextInput} input
*/
export function initPage(button, input) {
// your code goes here ...
button.onClick(() => { /*...*/ });
input.onInput(() => { /*...*/ });
}
Now, when we start using the initPage()
function on the page code file, we can see autocomplete list.
Velo: autocomplete suggestion list
After typing the first $
symbol, we see a list of the suggestions. We can move on the list with ↑ ↓ keys and select one with ↵ Enter key.
Also, we can see the initPage()
function has arguments types validation.
Velo: type error, a function expect an $w.TextInput
type instead $w.Page
It's very cool! Now, I can sleep calmly 😀
Interface as a function param
Suppose, we want to use more than 2 arguments in the initPage()
function. In this case, I guess better to use an object as an argument and put elements to object property. With object argument, we don't depend on the order of params. An object has more flexibility if we want to add or remove a new element.
Here we can use an interface syntax. It's similar to CSS syntax, where we describe a key name and types inside curly braces
@param { { name1: type; name2: type; … } } paramName
public/initPage.js
// Filename: public/initPage.js
/**
* @param {{
* button: $w.Button;
* input: $w.TextInput;
* text: $w.Text;
* box: $w.Box;
* }} elements
*/
export function initPage({
button,
input,
text,
box,
}) {
// your code goes here ...
button.onClick(() => { /*...*/ });
input.onInput(() => { /*...*/ });
}
We have the autocomplete for the object keys and values. Very useful.
Velo: autocomplete and type validation
Resources
- Official documentation for JSDoc 3
- TypeScript: Documentation - JSDoc Reference
- JSDoc Cheatsheet and Type Safety Tricks
Posts
Posted on February 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.