---
title: "Rocket Chat Dev Conventions Guide"
slug: "conventions"
description: "Follow Rocket Chat development conventions. Maintain secure, consistent code across collaboration projects."
updated: 2026-02-19T11:53:36Z
published: 2026-02-19T16:07:39Z
---

> ## Documentation Index
> Fetch the complete documentation index at: https://developer.rocket.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Conventions

Rocket.Chat follows a set of coding conventions to ensure that our codebase remains **clear, consistent, and maintainable**. Adhering to these guidelines helps improve collaboration, reduces technical debt, and makes the project easier to scale.

Below are some important recommendations to follow when writing TypeScript.

## TypeScript general tips

### Avoid CommonJS features

Prefer **ES Modules** over CommonJS when writing TypeScript. Avoid using CommonJS constructs such as `require` and `module.exports` alongside ES module syntax like `import` and `export`.

ES modules offer better portability, improved tooling support, and align with modern JavaScript standards.

> [!NOTE]
> Unlike CommonJS, ES modules do not support synchronous conditional imports.

**Valid: CommonJS conditional export**

CommonJS supports conditional loading because `require` executes at runtime.

```typescript
// commonjs.ts

if (condition) {
  const foo = require('foo');
  module.exports = foo;
} else {
  module.exports = {};
}
```

**Invalid: Conditional ES module import**

ES module imports are statically analyzed and must be declared at the top level. Conditional imports like the example below will not compile.

```typescript
// esmodule.ts

if (condition) {
  import foo from 'foo';
  export default foo;
} else {
  export default {};
}
```

---

### Prefer `import type` over `import`

When importing **types only**, always use `import type` instead of a regular `import`.

Although the standard `import` syntax works in both JavaScript and TypeScript, it has runtime implications. A regular `import` may cause the imported module to be included in the generated bundle, even if it is only used for type checking.

In contrast, `import type` is erased during compilation. It is a TypeScript-only construct and ensures that no unnecessary runtime code is emitted.

Using `import type` helps:

- Prevent unintended module inclusion in the bundle
- Reduce bundle size
- Make type-only dependencies explicit
- Improve clarity between runtime and type usage

**Examples**:

`Foo.ts`

```typescript
export class Foo {
  bar: string;

  constructor(bar: string) {
    this.bar = bar;
  }
}
```

`Bar.ts`

```typescript
export class Bar {
  foo: Foo;

  constructor(foo: Foo) {
    this.foo = foo;
  }
}
```

`index.ts`

```typescript
import { Foo } from './Foo';
import type { Bar } from './Bar';

declare const foo: Foo;
declare const bar: Bar;
```

**Transpiled output (**`index.js`**)**

```javascript
import { Foo } from './Foo';
```

Notice that `Bar` is not included in the emitted JavaScript because it was imported using `import type`.

## Avoid using classes as namespaces

Avoid using classes purely as containers for functions (i.e., as namespaces).

**Do not use this pattern:**

```typescript
// foo.ts
class Foo {
  bar(): void {
    // ...ts
  }
}

export const foo = new Foo();

// index.ts
import { foo } from './foo';

foo.bar();
```

In this example, the class is used only to group functions. It does not encapsulate meaningful state or behavior. This adds unnecessary abstraction and complexity.

**Preferred approach: use the module as the singleton**

When you only need to group related functions or variables, use the module itself as the namespace.

```typescript
// foo.ts
export function bar(): void {
  // ...
}

// index.ts
import * as foo from './foo';

foo.bar();
```

This approach:

- Keeps the code simpler
- Avoids unnecessary instantiation
- Improves readability and tree-shaking

**Valid exception: managing internal state**

A class is acceptable when it encapsulates internal state and provides controlled access to it.

```typescript
// foo.ts
class Foo {
  baz: number;

  bar(): void {
    // perform actions referencing and modifying `this.baz`
  }
}

export const foo = new Foo();

// index.ts
import { foo } from './foo';

foo.bar();
```

In this case, using a class is justified because:

- It manages internal state (`baz`)
- It provides a controlled interface (`bar`)
- It represents meaningful behavior, not just grouping

### Avoid using `any` (except as a generic constraint)

Avoid using `any` in most cases. Instead, prefer safer alternatives such as `unknown`:

- `unknown` is the universal type and represents any possible value.
- `any` is not a real type, it disables TypeScript’s type checking.
- Using `any` removes type safety and can hide bugs.
- Using `unknown` forces proper type narrowing before usage.

**Example:**`any`**vs**`unknown`

❌ Using `any` :

```typescript
// Avoid using any
declare const foo: any;

foo.bar(); // No compilation error
```

Because `foo` is `any`, TypeScript allows any operation, even if it’s unsafe.

Using `unknown` :

```typescript
// Prefer using unknown
declare const bar: unknown;

bar.baz(); // Compilation error
```

TypeScript correctly prevents unsafe access.

You must narrow the type first:

```typescript
const hasBaz = (bar: unknown): bar is { baz(): void } =>
  typeof bar === 'object' &&
  bar !== null &&
  'baz' in bar &&
  typeof (bar as { baz: unknown }).baz === 'function';

if (hasBaz(bar)) {
  bar.baz(); // No compilation error
}
```

This results in safer and more explicit code.

**Valid exception: generic type constraints**

An important exception is when `any` is used as part of a **generic constraint**, particularly with function types:

```typescript
type X<F> = F extends (x: unknown) => void ? true : false;
type Y<F> = F extends (x: any) => void ? true : false;

type A = X<(x: string) => void>; // `false`, because x is not `unknown`
type B = Y<(x: string) => void>; // `true`, because x is anything
```

In this scenario:

- `unknown` is restrictive.
- `any` allows broader compatibility.
- Using `any` here is intentional and correct.

---

## Migrating from JavaScript

### TypeScript is a superset of JavaScript

TypeScript builds on top of JavaScript. This means that when migrating from JavaScript to TypeScript, you can continue using the same syntax and patterns you’re already familiar with.

Tools like the TypeScript compiler (`tsc`) and `eslint` may report additional warnings or errors. Most of these are designed to enforce best practices and improve code quality. In many cases, they help identify potential issues early. Some warnings can be temporarily ignored during migration, especially if they do not affect runtime behavior, but they should be addressed over time.

### JSDoc

When the `allowJs` option is enabled in your `tsconfig.json`, TypeScript can analyze JavaScript files and understand type information provided through JSDoc comments.

This is particularly useful during gradual migration from JavaScript to TypeScript. It allows you to introduce type safety without immediately converting files to `.ts`.

Consider the following JavaScript example:

```javascript
// module.js
/**
 * @typedef {Object} Foo
 * @property {string} bar
 * @property {string} qux
 */
export const foo = {
  bar: 'baz'
};

foo.qux = 'quux';
```

By default, `tsc` may infer the type of `foo` as `{ bar: string }`, ignoring the `qux` property because it is added later. This results in incomplete type information.

To ensure TypeScript understands the full structure of the object, you can explicitly define the type using JSDoc.

Alternatively, you can use the `@type` tag with syntax similar to TypeScript’s `type` declarations:

```javascript
// module.js
/**
 * @type {{ bar: string; qux: string }}
 */
export const foo = {
  bar: 'baz'
};

foo.qux = 'quux';
```

Both approaches help TypeScript correctly recognize the intended structure of `foo`.

Using JSDoc in this way makes JavaScript files more type-aware and helps smooth the transition to TypeScript without requiring a full rewrite.

### Declare a `*.d.ts` file

When migrating larger JavaScript modules to TypeScript, it is strongly recommended to start by creating a declaration file (`.d.ts`).

While the migration process can be complex, a dedicated declaration file acts as a clear contract for your module. TypeScript uses `.d.ts` files to understand the shape of a module, including its exports and public API, without requiring the implementation to be rewritten immediately.

You can think of a `.d.ts` file as the **interface of a module**. It defines what the module exposes, while the actual logic can remain in JavaScript during the transition phase.

For large modules, creating a declaration file also helps you:

- Break down responsibilities into smaller, more manageable pieces
- Clarify the module’s public surface
- Plan refactoring before fully converting the implementation
- Go beyond what JSDoc can realistically provide in complex cases

Because of these benefits, starting with a `*.d.ts` file is highly recommended when converting significant JavaScript modules to TypeScript.

Here’s an example of a `*.d.ts` file for a hypothetical module:

```typescript
// hugeModule.d.ts
export function foo(): void; // maybe it will be placed in another module
export function bar(): void; // maybe it will be placed in another module
```

In this example, the declaration file defines the exported API without including any implementation details. This allows TypeScript to type-check consumers of the module even before the module itself is fully migrated.

## React

> [!NOTE]
> Most of the recommendations in this section are inspired by [Alex Kondov's *Tao of React*](https://alexkondov.com/tao-of-react/)*.*

### Components

1. **Prefer functional components**

React originally introduced class components to leverage JavaScript class syntax for managing state and lifecycle methods. However, class components have notable drawbacks:

With the introduction of Hooks, React provided a simpler and more flexible way to manage state and side effects. Hooks preserve the core idea of components as render functions, eliminating the need for classes and streamlining development.
  - They tend to be more verbose.
  - They often encourage misuse of inheritance through `extends` and `super`.
2. **Declare one component per file**

Although [colocation](https://kentcdodds.com/blog/colocation) can be useful, it is not consistently recommended for defining multiple React components within a single file.

In practice, this pattern is often misused. What starts as a small addition (for example, adding a modal next to a page component) can quickly grow into a cluttered file with loosely related components, making maintenance and readability more difficult. Keeping one component per file improves clarity, organization, and long-term maintainability.
3. **Name components**

Failing to name a component is a common mistake that can significantly impact debugging.

Unnamed components result in:

There are two recommended ways to properly name a component:
  - Less informative stack traces
  - Reduced clarity in React DevTools
  - Harder navigation when inspecting component trees
  - **Option 1: Use a named function:**

```typescript
const Foo = () => {
  return <div>Foo</div>;
};

console.log('The component name is:', Foo.name);
```
  - **Option 2: Set the**`displayName`**property:**

```typescript
const Foo = memo(() => {
  return <div>Foo</div>;
});

Foo.displayName = 'Foo'; // `Foo.name` is `undefined`

console.log('The component name is:', Foo.displayName);
```
4. **Use default export at the end of file**

Although named exports are often preferred in general, using a **default export for React components** improves readability and works especially well with Higher-Order Components (HOCs) such as `memo` and `forwardRef`. It also integrates more cleanly with code-splitting patterns like `React.lazy`.

**Recommended approach:**

This pattern:

```typescript
// Component.tsx

import { memo } from 'react';

type ComponentProps = {
  name: string;
};

// It is NOT an anonymous function
const Component = (props: ComponentProps) => {
  return <div>Hello, {props.name}</div>;
};

export default memo(Component); // the component name is preserved
```

```typescript
// index.ts

import { lazy } from 'react';

const Component = lazy(() => import('./Component'));
```

This approach avoids extra wrapping logic when using `lazy`.

---

**Less readable alternative (named export)**

Using named exports with HOCs often leads to:

```typescript
// Component.tsx

import { memo } from 'react';

type ComponentProps = {
  name: string;
};

// It is an anonymous function
export const Component = memo((props: ComponentProps) => {
  return <div>Hello, {props.name}</div>;
});

Component.displayName = 'Component'; // needed for React Dev Tools
```

```typescript
// index.ts

import { lazy } from 'react';

const Component = lazy(async () => {
  const { Component } = await import('./Component');
  return { default: Component }; // need to reconstruct the default export
});
```

Here, you must manually reconstruct a default export to make `lazy` work correctly.
  - Preserves the component’s name
  - Keeps the file structure clean
  - Works naturally with `lazy` imports
  - Anonymous component functions
  - Manual `displayName` assignment
  - Extra work when using `lazy`
5. **Extract helper functions**

With the adoption of React Hooks, it’s common to define helper functions directly inside components. This is convenient because functions can access variables from the surrounding scope without explicitly receiving them as arguments. However, this pattern has downsides:

```typescript
const Component = () => {
  const value = useMyHook();

  const isValueOK = () => value === 'OK';

  return isValueOK() ? <>OK</> : null;
};
```

Helper functions are generally expected to be **pure,**meaning their output depends only on their inputs.

In the example above:

While redefining functions per render is usually inexpensive, it can introduce subtle complexity when optimizing components.

**Preferred approach**

Extract helper functions and pass dependencies explicitly:

```typescript
const isValueOK = (value: string) => value === 'OK';

const Component = () => {
  const value = useMyHook();

  return isValueOK(value) ? <OK /> : null;
};
```

This is better because:

  - The helper is **pure** and easier to test.
  - Dependencies are explicit.
  - The function is not recreated on every render.
  - The component becomes easier to reason about.
  - `isValueOK` implicitly depends on `value` from the outer scope.
  - This makes the function less explicit and therefore less predictable.
  - On every render, the function is recreated.
  - If passed as a prop, it may cause unnecessary re-renders unless wrapped in `useCallback`.
