Skip to content

Module structure

Jiří Cihelka edited this page Dec 30, 2023 · 2 revisions

A big codebase needs to be split into multiple files. We achieve this by using modules.
Every file is a module, that provides some exports (functions, classes, constants, etc.) and can import other modules.

Exporting

We use the export keyword to export something from a module. We don't use the export default keyword to be consistant as it cannot always be used without using a namespace. We don't use namespace to improve the readability of the code.

Exporting can look something like this:

export const PI = 3.14;
export function add(lhs: number, rhs: number): number {
    return lhs + rhs;
}
export class Foo {
    constructor(public bar: number) {}
}

Names that aren't prefixed with export are not exported from the module. They are however still available in the module and can be used in the module.

We can also export directly from an import (this is referred to as reexporting in this documentation):

export { add } from "./add";
export { PI } from "./constants";
export { Foo } from "./Foo";

In this case, the add function, the PI constant and the Foo class are imported from the ./add, ./constants and ./Foo modules and then exported from the current module. We don't have access to theses names in the current module.

Importing

All public names from modules should be reexported by their index.ts files. If we need to import something from a module, we should find the most outer index.ts file, that reexports the name we need, and also doesn't cause a circular import. We should then import the name from this index.ts file.
Importing is done using the import keyword. We can import a name from a module like this:

import { add } from "./add";

If we want to import multiple names from a module, we can do it like this:

import { add, PI } from "./add";

If we have a name conflict, we can rename the imported name like this:

import { PI, add as addNumbers } from "./add";
import { add as addStrings } from "./addString";

Importing types

If we only need a type from a module, we can import it using the import type syntax. When possible, this is prefered over a non-type import, because it doesn't import the name at runtime. This results in better performance and allow for circular imports.

import type { Foo } from "./Foo";

This is useful, for example, when we want to use a type in a type annotation or JSDoc comment.

import type { Foo } from "./Foo";

/**
 * This class is a wrapper around the @link{Foo} class.
 */
class Bar {
    constructor(public foo: Foo) {}
}

All import type imports are removed from the compiled code.

Location of statements

To be consistent our import and export statements should be sorted in this way:

  1. import type statements
  2. import statements
  3. export from statements
  4. The rest of the code (including export statements)

All of these sections should be seperated by a newline.

Example:

import type { Foo } from "./Foo";
import type { Bar } from "./Bar";

import { add } from "./add";
import { PI } from "./constants";

export { sub } from "./sub";

export function mult(lhs: number, rhs: number): number {
    return lhs * rhs;
}