Type hinting overview
The Frontend codebase of the GitLab project currently does not require nor enforces types. Adding type annotations is optional, and we don’t currently enforce any type safety in the JavaScript codebase. However, type annotations might be very helpful in adding clarity to the codebase, especially in shared utilities code. This document aims to cover how type hinting currently works, how to add new type annotations, and how to set up type hinting in the GitLab project.
JSDoc
JSDoc is a tool to document and describe types in JavaScript code, using specially formed comments. JSDoc’s types vocabulary is relatively limited, but it is widely supported by many IDEs.
Examples
Describing functions
Use @param
and @returns
to describe function type:
/**
* Adds two numbers
* @param {number} a first number
* @param {number} b second number
* @returns {number} sum of two numbers
*/
function add(a, b) {
return a + b;
}
Optional parameters
Use square brackets []
around a parameter name to mark it as optional. A default value can be
provided by using the [name=value]
syntax:
/**
* Adds two numbers
* @param {number} value
* @param {number} [increment=1] optional param
* @returns {number} sum of two numbers
*/
function increment(a, b=1) {
return a + b;
}
Object parameters
Functions that accept objects can be typed by using object.field
notation in @param
names:
/**
* Adds two numbers
* @param {object} config
* @param {string} config.path path
* @param {string} [config.anchor] anchor
* @returns {string}
*/
function createUrl(config) {
if (config.anchor) {
return path + '#' + anchor;
}
return path;
}
Annotating types of variables that are not immediately assigned a value
For tools and IDEs it’s hard to infer type of a value that doesn’t immediately receive a value. We
can use @type
notation to assign type to such variables:
/** @type {number} */
let value;
Consult JSDoc official website for more syntax details.
Tips for using JSDoc
Use lower-case names for basic types
While both uppercase Boolean
and lowercase boolean
are acceptable, in most cases when we need a
primitive or an object — lower case versions are the right choice: boolean
, number
, string
,
symbol
, object
.
/**
* Translates `text`.
* @param {string} text - The text to be translated
* @returns {string} The translated text
*/
const gettext = (text) => locale.gettext(ensureSingleLine(text));
Use well-known types
Well-known types, like HTMLDivElement
or Intl
are available and can be used directly:
/** @type {HTMLDivElement} */
let element;
/**
* Creates an instance of Intl.DateTimeFormat for the current locale.
* @param {Intl.DateTimeFormatOptions} [formatOptions] - for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
* @returns {Intl.DateTimeFormat}
*/
const createDateTimeFormat = (formatOptions) =>
Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
Import existing type definitions via import('path/to/module')
Here are examples of how to annotate a type of the Vue Test Utils Wrapper variables, that are not immediately defined:
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
// ...
wrapper = mountExtended(/* ... */);
/** @type {import('@vue/test-utils').Wrapper} */
let wrapper;
// ...
wrapper = shallowMount(/* ... */);
import()
is not a native JSDoc construct, but it is
recognized by many IDEs and tools. In this case we’re aiming for better clarity in the code and
improved Developer Experience with an IDE.JSDoc is limited
As was stated above, JSDoc has limited vocabulary. And using it would not describe the type fully. But sometimes it’s possible to use 3rd party library’s type definitions to make type inference to work for our code. Here’s an example of such approach:
- export const mountExtended = (...args) => extendedWrapper(mount(...args));
+ import { compose } from 'lodash/fp';
+ export const mountExtended = compose(extendedWrapper, mount);
Here we use TypeScript type definitions from compose
function, to add inferred type definitions to
mountExtended
function. In this case mountExtended
arguments will be of same type as mount
arguments. And return type will be the same as extendedWrapper
return type.
We can still use JSDoc’s syntax to add description to the function, for example:
/** Mounts a component and returns an extended wrapper for it */
export const mountExtended = compose(extendedWrapper, mount);
System requirements
A setup might be required for type definitions from GitLab codebase and from 3rd party packages to be properly displayed in IDEs and tools.
Aliases
Our codebase uses many aliases for imports. For example, import Api from '~/api';
would import a
app/assets/javascripts/api.js
file. But IDEs might not know that alias and thus might not know the
type of the Api
. To fix that for most IDEs — we need to create a
jsconfig.json
file.
There is a script in the GitLab project that can generate a jsconfig.json
file based on webpack
configuration and current environment variables. To generate or update the jsconfig.json
file —
run from the GitLab project root:
node scripts/frontend/create_jsconfig.js
jsconfig.json
is added to gitignore list, so creating or changing it does not cause Git changes in
the GitLab project. This also means it is not included in Git pulls, so it has to be manually
generated or updated.
3rd party TypeScript definitions
While more and more libraries use TypeScript for type definitions, some still might have JSDoc
annotated types or no types at all. To cover that gap, TypeScript community started a
DefinitelyTyped initiative, that creates and
supports standalone type definitions for popular JavaScript libraries. We can use those definitions
by either explicitly installing the type packages (yarn add -D "@types/lodash"
) or by using a
feature called Automatic Type Acquisition (ATA),
that is available in some Language Services
(for example, ATA in VS Code).
Automatic Type Acquisition (ATA) automatically fetches type definitions from the DefinitelyTyped
list. But for ATA to work, a globally installed npm
might be required. IDEs can provide a fallback
configuration options to set location of the npm
executables. Consult your IDE documentation for
details.
Because ATA is not guaranteed to work and Lodash is a backbone for many of our utility functions
— we have DefinitelyTyped definitions for Lodash
explicitly added to our devDependencies
in the package.json
. This ensures that everyone gets
type hints for lodash
-based functions out of the box.