This code style applies to projects which the element-web team directly maintains or is reasonably adjacent to. As of writing, these are:
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk has stricter code organization to reduce the maintenance burden. These projects will declare their code style within their own repos.
Note that some requirements will be layer-specific. Where the requirements don't make sense for the project, they are used to the best of their ability, used in spirit, or ignored if not applicable, in that order.
Unless otherwise specified, the following applies to all code:
When a statement's body is a single line, it must be written without curly braces, so long as the body is placed on the same line as the statement.
if (x) doThing();
Blocks for if, for, switch and so on must have a space surrounding the condition, but not
within the condition.
if (x) {
doThing();
}
lowerCamelCase is used for function and variable naming.
UpperCamelCase is used for general naming.
Interface names should not be marked with an uppercase I.
One variable declaration per line.
If a variable is not receiving a value on declaration, its type must be defined.
let errorMessage: Optional<string>;
Objects can use shorthand declarations, including mixing of types.
{
room,
prop: this.prop,
}
// ... or ...
{ room, prop: this.prop }
Object keys should always be non-strings when possible.
{
property: "value",
"m.unavoidable": true,
[EventType.RoomMessage]: true,
}
If a variable's type should be boolean, make sure it really is one.
const isRealUser = !!userId && ...; // good
const isRealUser = Boolean(userId) && Boolean(userName); // also good
const isRealUser = Boolean(userId) && isReal; // also good (where isReal is another boolean variable)
const isRealUser = Boolean(userId && userName); // also fine
const isRealUser = Boolean(userId || userName); // good: same as &&
const isRealUser = userId && ...; // bad: isRealUser is userId's type, not a boolean
if (userId) // fine: userId is evaluated for truthiness, not stored as a boolean
Use switch statements when checking against more than a few enum-like values.
Use const for constants, let for mutability.
Describe types exhaustively (ensure noImplictAny would pass).
Declare member visibility (public/private/protected).
Private members are private and not prefixed unless required for naming conflicts.
Prefer readonly members over getters backed by a variable, unless an internal setter is required.
Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
Note that an explicit type is optional if not expected to be used outside of the function call, unlike in this example:
interface MyObject {
hasString: boolean;
}
type Options = MyObject | string;
function doThing(arg: Options) {
// ...
}
Variables/properties which are public static should also be readonly when possible.
Interface and type properties are terminated with semicolons, not commas.
Prefer arrow formatting when declaring functions for interfaces/types:
interface Test {
myCallback: (arg: string) => Promise<void>;
}
Prefer a type definition over an inline type. For example, define an interface.
Always prefer to add types or declare a type over the use of any. Prefer inferred types
when they are not any.
any, a comment explaining why must be present.import should be used instead of require, as require does not have types.
Export only what can be reused.
Prefer a type like Optional<X> (type Optional<T> = T | null | undefined) instead
of truly optional parameters.
A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
? operator is inappropriate is when taking a room ID: typically the caller should
supply the room ID if it knows it, otherwise deliberately acknowledge that it doesn't
have one with null.
function doThingWithRoom(
thing: string,
room: Optional<string>, // require the caller to specify
) {
// ...
}
There should be approximately one interface, class, or enum per file unless the file is named "types.ts", "global.d.ts", or ends with "-types.ts".
Bulk functions can be declared in a single file, though named as "foo-utils.ts" or "utils/foo.ts".
Imports are grouped by external module imports first, then by internal imports.
File ordering is not strict, but should generally follow this sequence:
Variable names should be noticeably unique from their types. For example, "str: string" instead of "string: string".
Use double quotes to enclose strings. You may use single quotes if the string contains double quotes.
const example1 = "simple string";
const example2 = 'string containing "double quotes"';
Prefer async-await to promise-chaining
async function () {
const result = await anotherAsyncFunction();
// ...
}
Avoid functions whose fundamental behaviour varies with different parameter types.
Multiple return types are fine, but if the function's behaviour is going to change significantly,
have two separate functions. For example, SDKConfig.get() with a string param which returns the
type according to the param given is ok, but SDKConfig.get() with no args returning the whole
config object would not be: this should just be a separate function.
Inheriting all the rules of TypeScript, the following additionally apply:
Props interface declared immediately above them. It can be
empty if the component accepts no props.State interface declared immediately above them, but after Props.React.ComponentProps<typeof ComponentNameHere>
instead.Stores should use a singleton pattern with a static instance property:
class FooStore {
public static readonly instance = new FooStore();
// or if the instance can't be created eagerly:
private static _instance: FooStore;
public static get instance(): FooStore {
if (!FooStore._instance) {
FooStore._instance = new FooStore();
}
return FooStore._instance;
}
}
Stores must support using an alternative MatrixClient and dispatcher instance.
Utilities which require JSX must be split out from utilities which do not. This is to prevent import cycles during runtime where components accidentally include more of the app than they intended.
Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities if at all possible.
A component should only use CSS class names in line with the component name.
Curly braces within JSX should be padded with a space, however properties on those components should not. See above code example.
Functions used as properties should either be defined on the class or stored in a variable. They should not be inline unless mocking/short-circuiting the value.
Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure which should be used.
Write more views than structures. Structures are chunks of functionality like MatrixChat while views are isolated components.
Components should serve a single, or near-single, purpose.
Prefer to derive information from component properties rather than establish state.
Do not use React.Component::forceUpdate.
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
mx_MyFoomx_MyFoo_avatarmx_MyFoo_avatarUsermx_MyFoo_avatar--user$font variables instead of manual values.Use the whole class name instead of shortcuts:
.mx_MyFoo {
& .mx_MyFoo_avatar {
// instead of &_avatar
// ...
}
}
Break multiple selectors over multiple lines this way:
.mx_MyFoo,
.mx_MyBar,
.mx_MyFooBar {
// ...
}
Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be documented for what the values mean:
.mx_MyFoo {
width: calc(100% - 12px); // 12px for read receipts
top: -2px; // visually centred vertically
z-index: 10; // above user avatar, but below dialogs
}
Avoid the use of !important. If !important is necessary, add a comment explaining why.
Use the following convention template:
// Describe the class, component, or file name.
describe("FooComponent", () => {
// all test inspecific variables go here
beforeEach(() => {
// exclude if not used.
});
afterEach(() => {
// exclude if not used.
});
// Use "it should..." terminology
it("should call the correct API", async () => {
// test-specific variables go here
// function calls/state changes go here
// expectations go here
});
});
// If the file being tested is a utility class:
describe("foo-utils", () => {
describe("firstUtilFunction", () => {
it("should...", async () => {
// ...
});
});
describe("secondUtilFunction", () => {
it("should...", async () => {
// ...
});
});
});
As a general principle: be liberal with comments. This applies to all files: stylesheets as well as JavaScript/TypeScript.
Good comments not only help future readers understand and maintain the code; they can also encourage good design by clearly setting out how different parts of the codebase interact where that would otherwise be implicit and subject to interpretation.
Aim to document all types, methods, class properties, functions, etc, with TSDoc doc comments.
This is especially important for public interfaces in matrix-js-sdk, but is good practice in general.
Even very simple interfaces can often benefit from a doc-comment, both as a matter of consistency, and because simple interfaces have a habit of becoming more complex over time.
React components should be documented in the same way as other classes or functions. The documentation should give a brief description of how the component should be used, and, especially for more complex components, each of its properties should be clearly documented.
Inside a function, there is no need to comment every line, but consider:
When making changes to existing code, authors are expected to read existing comments and make any necessary changes to ensure they remain accurate.
Reviewers are encouraged to consider whether more comments would be useful, and to ask the author to add them.
It is natural for an author to feel that the code they have just written is "obvious" and that comments would be redundant, whereas in reality it would take some time for reader unfamiliar with the code to understand it. A reviewer is well-placed to make a more objective judgement.