Skip to content

Latest commit

 

History

History
259 lines (170 loc) · 9.14 KB

File metadata and controls

259 lines (170 loc) · 9.14 KB

Coding Guidelines (TypeScript & JavaScript)

Basically, we use Eslint to ensure as many of these guidelines as possible. Unfortunately, not everything can be automated. It is therefore important that we know and live these guidelines.

The guidelines apply in particular to TypeScript and JavaScript. However, it would be good if they were also applied to other languages as far as possible.

If a guideline cannot be implemented or is not implemented for certain reasons, this should be recorded in a code comment.

The most important function first

The most important function of the file should always be placed as high up as possible. Followed by the second most important, etc.

Separation of Concerns

If possible, we do not want to have more than one single export statement per file. This must not be a default export. There are individual exceptions where several export statements are desirable. However, this tends to be the exception.

Model files in particular (interfaces, types, schemas, etc.) should ** ALWAYS** be created in their own files, preferably in a model folder or even in the model package. This way we can use them everywhere without worrying about circular dependencies.

Exception for Zod schemas: Here it makes sense to export the type directly in the schema as well.

Independent code lines

To avoid Git conflicts, code lines should always be as independent as possible. In concrete terms, this means

  • Empty line at the end of the file
  • Always add a comma and semicolon at the end of the line if possible

Most of this can be ensured with Prettier and Eslint.

Avoid Nesting

Nesting should always be avoided as far as possible. Early returns can be used for this purpose, for example.

https://www.youtube.com/watch?v=CFRhGnuXG-4

Read-only by default

Variables, properties, etc. should always be read-only if possible. In concrete terms, this means

Separate files for functions and types

To avoid Git conflicts, optimise readability and separation of concern, it is important that we create separate files for all functions and types that are exported.

Functions and types that are only used internally (private) should be in the same file as the exported function / type ( public).

File names

If possible, the file names should always correspond to the function / interface / class. This makes the code easier to understand.

This also applies to NestJS projects or other 3rd party projects. With NestJS, this does not correspond to their conventions, but the demand from the community is high and there should soon be a solution for this too: Add configuration option to generate files with PascalCase instead of the default naming convention - Issue #462 - nestjs/nest-cli

Self-Documented Code

With good naming and meaningful comments, further documentation can be reduced.

Avoid external node packages

External node packages should only be used if it makes absolute sense. This means if it is complex code or if the code is managed by a vendor. Everything that can be done with native functions and a few lines of code should be copied and referenced accordingly so that we know where the code comes from. The decision should be based on common sense.

References and credits

If we copy code from Stackoverflow or other places, please always leave a reference. (even if the code is heavily customised)

If some code is generated by AI, please also leave a reference. An exception can be made if the code is heavily modified.

Avoid destructing assignment

Destructing assignment should be avoided and only used where it really makes sense.

What is destructing assignment? Destructuring assignment - JavaScript | MDN

Why should destructuring assignment be avoided? Destructing assignment makes code less maintainable. It is also usually less readable.

Examples:

  • Bad: async ({ request }), Good: async (args: DataFunctionArgs)
  • Bad: ({firstname}: {firstname:string}) => {, Good: (form: FormData) => {
  • Bad: Object.entries(data).map(entry => entry[0] + entry[1]), Good: Object.entries(data).map(([key, value]) => key + value])

Execution: For arrays with fixed elements, Destructing Assignment is used for readability, as each place (index) is assigned to a variable.

Addition: Shorthand properties should be avoided for object assignments:

  • Bad: const demo = {a, b, c}
  • Good: const demo = { a: a, b: b, c: c }

Although you write more code this way, it is clearer what happens.

Exception: dynamic keys

If it does not matter what the keys are called later, destructing assignment is permitted. For example, if everything is logged 1-to-1. However, if a schema or the following code specifies what the parameters should be called, destructing assignment must not be permitted.

Exception: Translations

For translations, it is okay to do something like this: const { t } = this.props;

The following reasoning:

  • We have always done it this way with t. That way we stay consistent.
  • A good naming like translationService.translate(...) would worsen the readability of the whole HTML.

No default exports

Default exports should always be avoided if possible. If there is no other option, at least a meaningful name should be used.

Conditional (ternary) operator must NEVER be nested

The use of the conditional operator is okay if it helps readability and comprehension. However, it must NEVER be nested.

Makes sense:

const input = condition ? a : b;

Forbidden:

const input = condition1 ? a : condition2 ? b : c;

Nested conditional operators are difficult to read and understand. In addition, errors happen quickly. An alternative would be, for example, a separate function or an IF-Else statement.

Prefer type (with readonly) over interface

We prefer types over when defining object structures. In addition, we always want to use readonly. (See Read-only by default)

Preferred:

type Demo = {
  name: string;
};

Only when absolutely needed:

interface Demo {
  readonly name: string;
}

Lecture:

Arrow functions vs. function expressions

For most cases, arrow functions are preferred over function expressions. Arrow functions are more concise and easier to read.

It's recommended to use regular function in any of the following cases:

  • when you need to use a constructor with the new keyword
  • when you need the binding to be dynamically scoped
  • when you want to use the arguments object

And you can use arrow functions in any of the following cases:

  • when you want a more concise syntax for the function
  • when you need to maintain the lexical scope of this
  • for non-method functions (in most cases)

Lecture:

Readonly Data Classes

The concept of data classes comes from programming languages such as Kotlin. The idea is that data becomes ‘immutable’. This means that they are only processed at certain points in the code. This massively increases the readability and maintainability of the code. That's why we want to apply this concept here too. But in a slightly weaker form, as it only works via TypeScript and is not intended to be used natively.

We have read-only interfaces for objects and then copy them. Theoretically, we could even do Object.freeze. This is up to the developer.

Definition:

type Demo = {
  readonly name: string;
  readonly value: number;
}

Data flow:

const a: Demo = {
  name: ‘A’,
  value: 1,
};
const b: Demo = {
  ...a,
  value: 2,
};

Translations in the frontend

In the frontend, we always do translations with {t(‘...’)} . The function can be fetched with a hook or in any other way. Destruction assignments may be used for this function as an exception: const { t } = useTolgee(); (more on this above).

We cannot use <T> in every context, and then we would have to use both in some cases, or we are not consistent everywhere.

Test-files location

Test-files should be located in the same folder as the file to be tested. This makes it easier to understand the context.

Avoid prop drilling

Prop drilling can tightly couple components together, making it harder to refactor or restructure the component hierarchy without affecting other parts of the application. This can result in decreased maintainability and flexibility.

Lecture: