Best Practices
Enable JSDoc Linting
ESLint supports linting of JSDoc comments. Enabling the rules on your component will help to ensure that you avoid missing or incorrect type definitions for function parameters.
You can check if your component is already covered by ensuring it is not in the
rollout-valid-jsdoc or rollout-require-jsdoc sections of the
eslint-rollouts.config.mjs
file in the top level of firefox-main.
Documenting Types
Avoid Missing Type Annotations
By default, if no type annotation is given and it cannot be inferred, then
TypeScript will assign that variable the any type. This is a special type that
skips type checking, and therefore may cause hidden failures.
For class members and functions this means adding type definitions for all the parameters, e.g.
class Foo {
/**
* Stores the search string. The type here could be omitted if the property is assigned in
* the constructor of the class.
*
* @type {string}
*/
#search;
/**
* Details about the function.
*
* @param {string} searchString
* Param documentation.
* @param {string[]} previousResult
* Param documentation.
* @param {nsIAutoCompleteObserver} listener
* Param documentation.
*/
startSearch(searchString, previousResult, listener) {}
}
Variables
Variable types will be inferred from the item that they are initially assigned to. However, sometimes you may need to define the type appropriately, especially for sets and maps.
// This will be inferred as type string.
let foo = "bar";
// This needs the type defining.
/** @type {Map<string, number>} */
let baz = new Map();
baz.set("star", 1701);
Avoid Object Types
Most Firefox projects turn noImplicitAny off (see
The tsconfig.json File). In those
projects, TypeScript treats object and Object in JSDoc as any. It doesn’t
check anything you read through such a value. So adding /** @type {object} */
to a variable silences every error on it.
Adding Types
Ideally all types should be defined. There are two ways to do this.
The first is within JSDoc comments:
/**
* @typedef {object} submissionMapEntry
* @property {SearchEngine} engine
* The search engine.
* @property {string} termsParameterName
* The search term parameter name.
*/
/**
* Alternately for function parameters:
*
* @param {object} options
* @param {boolean} options.option1
* A required boolean property within options.
* @param {number} [options.option2]
* An optional number property within options.
*/
function myFunc(options) {}
This may then be used within the file.
The second way may be more appropriate if a type is used widely within a
component. You can create a types/urlbarType.d.ts file, reference it from the
tsconfig.json file and include a definition such as:
/**
* A structure that holds the details of commands for results.
*/
type UrlbarResultCommand = {
/**
* The name of the command. Must be specified unless `children` is present.
* When a command is picked, its name will be passed as `details.selType` to
* `onEngagement()`. The special name "separator" will create a menu separator.
*/
name?: string;
/**
* An l10n object for the command's label. Must be specified unless `name`
* is "separator".
*/
l10n?: L10nIdArgs;
/**
* If specified, a submenu will be created with the given child commands.
*/
children?: UrlbarResultCommand[];
};
Note
If you are sharing object types outside of your component, prefer using a proper class definition or other structure.
Extend a Typedef with an Intersection
If a typedef gives a type in its braces, such as
@typedef {SearchDetails} AliasSearchDetails, TypeScript ignores the
@property tags that follow it. The typedef becomes another name for the type
in the braces. TypeScript then reports that each added property doesn’t exist on
that type.
To add properties, declare them in a second typedef and join the two with &:
/**
* @typedef {object} AliasDetails
* @property {string} alias
* The alias the user typed.
*/
/**
* @typedef {SearchDetails & AliasDetails} AliasSearchDetails
*/
Import Types from Other Files
You may import types from other files to be able to reuse them.
/**
* @import { LangTags } from "./translations.d.ts"
*/
/**
* @import { OpenedConnection } from "resource://gre/modules/Sqlite.sys.mjs"
*/
A *.d.ts file is a special type-definition file for TypeScript. These should
generally only be used for types that are internal to your component and are
not exposed outside.
Optional Values
Most Firefox projects turn strictNullChecks off. In those projects,
T|undefined is the same type as T. Adding or removing |undefined doesn’t
change which errors TypeScript reports.
It is worth keeping |undefined where a value can really be absent because it
informs the reader about the case. It may also be used in future if
strictNullChecks is turned on.
XPIDL Enums
Refer to XPIDL enums through their interfaces, for example
{nsILoadInfo.SchemelessInputType}.
tools/@types/generated/lib.gecko.xpcom.d.ts also declares a name
without the dot, nsILoadInfo_SchemelessInputType. That name isn’t global, so
TypeScript can’t find it.
Browser Window Values
Annotate a value that holds a browser window as BrowserWindow. BrowserWindow
declares the globals of browser.xhtml, such as gBrowser, with their
appropriate types. On a plain Window, every property that Window doesn’t
declare is any, including gBrowser.
To use BrowserWindow, add
tools/@types/browser-window-globals.d.ts to include. A project
that includes tools/@types/browser-window.d.ts already has it.
BrowserWindow still treats an undeclared name as any, including a misspelled
one (see What Might a Low Error Count Hide?).
If code reads a global that BrowserWindow lacks, add it to the
BrowserWindow namespace in browser-window-globals.d.ts. If modules that run
in the window read it as a bare global, also add a matching declare var line
to browser-window.d.ts.
Custom Elements
TypeScript doesn’t see an element class that a script loads with
Services.scriptloader.loadSubScript, because no module imports the script. So
each access to a member that the class defines is an error.
Convert the script to a module if you can. Other files can then import the class’s type, but TypeScript reports errors in the module itself only if the project includes it (see The tsconfig.json File). Until the script is a module, declare an interface for the element in the project’s shared declarations (see Shared Declarations). browser/components/tabbrowser/types/elements.ts is an example.
tools/@types/lib.gecko.augmentations.d.ts declares
MozXULElement, MozHTMLElement, MozBrowser and the MozElements object for
every project. MozElements is a constant with an inline object type, which
TypeScript can’t extend from another file. Add a missing member to
lib.gecko.augmentations.d.ts itself.
That file affects every project, so check all of them afterwards:
./mach lint -l typescript .
Files with API Documentation
The modules under js_source_paths in docs/config.yml have
generated pages on Firefox Source Docs. Both jsdoc and TypeScript read their
comments, and the two
disagree about some types:
Write
{number}, not{integer}. jsdoc acceptsinteger, but TypeScript can’t find it.Write
{T|null}, not{?T}. TypeScript treats the two the same, but the API page shows{?T}as plainT.
XPCOMUtils.declareLazy / defineLazy
These are newer functions that combine the existing XPCOMUtils functions into formats that are compatible with TypeScript.
Defining lazy objects with these functions provides the appropriate information
for TypeScript to be able to know the types on the lazy objects. Otherwise the
lazy objects will be defined as any.
Note
Use these functions in the modules a TypeScript project checks, so that
TypeScript knows the types of their lazy objects. Until the functions
move to ChromeUtils,
you need to import XPCOMUtils for them.