Skip to main content

TypeScript coding style

Since 6.0

This page describes the coding style for the TypeScript and TSX sources under js/esm/src in Moodle core and plugins.

Moodle follows XO's rules for TypeScript, with a small number of overrides listed below. This page does not repeat XO's rules: the linter is the reference, and it tells you which rule a line breaks and how to fix it.

AMD and YUI JavaScript is not affected. It is still linted by ESLint, using the rules in .eslintrc.

Running the linter​

# Lint every TypeScript source
grunt xo

# Apply the fixes XO can make itself
grunt xo --fix

# Lint only some files
grunt xo --files=public/lib/js/esm/src/fetch.ts,public/lib/js/esm/src/url.ts

The build tasks lint first and stop if XO reports an error, so a bundle is never built from sources that do not pass. This applies to grunt react, grunt js and plain grunt. In grunt react:watch, each change is linted before the rebuild, and the rebuild is skipped while the sources have errors. For installing and running Grunt, see NodeJS and Grunt.

Warnings are reported but do not stop the build.

warning

Run XO through grunt xo, not npx xo. Without the source globs that Grunt supplies, XO also lints every Markdown and HTML file in the tree.

tip

--fix rewrites code, not just whitespace. Review the diff before you commit it.

Rules​

The rules are:

  • XO's TypeScript rules
  • the two classic rules from eslint-plugin-react-hooks:
    • react-hooks/rules-of-hooks, as an error
    • react-hooks/exhaustive-deps, as a warning
  • the Moodle overrides below

The configuration is in xo.config.mjs at the root of the Moodle repository. Each override has its reason next to it.

note

The newer React Compiler rules in eslint-plugin-react-hooks, such as react-hooks/refs and react-hooks/set-state-in-effect, are not enabled. eslint-plugin-react is not used either, because it does not run on the ESLint version that XO bundles. TypeScript already covers most of what it checks, such as prop types.

Moodle overrides​

An override is only added where following XO would change something Moodle needs to keep, such as a released API name, a wire format, or the supported browsers. A rule being inconvenient is not a reason to override it. Propose a new override on the tracker.

Moodle conventions​

Some overrides only apply the general Coding style, so they are not explained again here:

The others are specific to TypeScript:

RuleMoodle settingWhy
jsdoc/require-param, jsdoc/check-param-namescheckDestructured: falseAn options object needs its own @param, but its keys are described once on its type.
unicorn/prefer-httpsoffThe GPL header in core's TypeScript files, as in most of core, uses http:// URLs.
unicorn/filename-casecamelCase, PascalCase or snake_caseA filename is the module's import name. Components use PascalCase, utility modules camelCase, and existing modules such as react_autoinit keep snake_case.
unicorn/name-replacementsparams, utils, func, args, props and repository allowedThe longer names would be worse. params and utils are published names, props is React's own term, and repository is the name core uses for web service modules.
unicorn/consistent-boolean-nameoffOption names match the server-side parameters they are sent as, such as loginrequired.
@typescript-eslint/naming-conventionrelaxedComponents may use PascalCase, as JSX requires. Object keys and type members may use any case, because they often name something external, such as an HTTP header or a snake_case web service field.

Supported browsers​

esbuild does not downlevel or polyfill the bundles, so the code must already run in every browser listed under browserslist in package.json. These rules would otherwise suggest features that are too new:

RuleMoodle settingWhat it would suggest
unicorn/no-array-sortoffArray.prototype.toSorted() (ES2023)
unicorn/prefer-promise-with-resolversoffPromise.withResolvers() (ES2024)
unicorn/prefer-atoffArray.prototype.at() (ES2022)
unicorn/logical-assignment-operatorsoff??=, ||= and &&= (ES2021)
@typescript-eslint/prefer-nullish-coalescingignoreIfStatements: true??= in place of an if that assigns a default
require-unicode-regexprequireFlag: 'u'The regular expression v flag (ES2024)

The syntax ones matter most. An older browser cannot parse ??= or the v flag at all, so the whole module fails to load, not just the line that uses them.

Null​

XO bans the null type in favour of undefined. Moodle keeps null, because some values are null by contract:

  • the browser's Storage.getItem() returns null
  • React's useRef(null) uses null
  • a web service argument set to null is sent in the request, while undefined is left out

@typescript-eslint/no-restricted-types is configured with XO's list minus null.

Disabling a rule inline​

Sometimes code has a good reason to break a rule in one place. Disable the rule for that line only, and give the reason after --. XO reports a disable comment that has no reason, and one that no longer suppresses anything.

An example of correct behaviour
/* eslint-disable-next-line @typescript-eslint/naming-convention -- Released API name from core/pending. */
static async Promise<T>(
An example of incorrect behaviour
/* eslint-disable @typescript-eslint/naming-convention */
static async Promise<T>(

Use a block (eslint-disable and eslint-enable) only when several lines in a row need the same exception, and give the reason on the opening comment.

See also​