Ways of Working
In order to provide consistency and maintainability in application development we provide protocols and standards in code style, version control and deployment.
Application
This section provides protocols and standards on application structure and code style.
Definition Hierarchy
We adhere to a specific hierarchy when defining multiple functions or type definitions within a single file. This hierarchy ensures that functions used by other functions in the same file are declared before they are invoked. Consider the following example:
const simpleOperation = (a:number, b:number) => { return a + b;};
const complexOperation = () => { ... simpleOperation(10, 10); ...};Attempting the reverse arrangement would result in an undesirable configuration:
const complexOperation = () => { ... simpleOperation(10, 10); ...};
const simpleOperation = (a:number, b:number) => { return a + b;};This structural choice ensures that simple functions are consistently positioned at the top of the file, while complex functions reside at the bottom. Developers can rely on this pattern, facilitating a more intuitive and readable codebase.
Type Checking
When implementing control flow through type checking, it is advisable to employ type guards rather than directly checking type properties. Consider the following example:
type Panda = { color: "white" };type Duck = { color: "brown" };type Animal = Panda | Duck;
const animalOperation = (animal: Animal) => { if (animal.color === "brown") { // It is a duck; perform duck-related operations } else { // It is a panda; perform panda-related operations };};In this scenario, we refine the general type to handle a more specific case and execute operations based on the more detailed type. However, if changes are made to the type definition, your control flow may no longer function as intended:
type Duck = { color: "pink" };
const animalOperation = (animal: Animal) => { if (animal.color === "brown") { // This block will never execute } else { // Your duck is now performing panda-related actions! };};To mitigate this issue, we leverage type guards:
type Duck = { color: "brown" };
const isDuck = (animal: Animal): animal is Duck => { return animal.color === "brown"};
const animalOperation = (animal: Animal) => { if (isDuck(animal)) { // It is a duck; perform duck-related operations } else { // It is a panda; perform panda-related operations };};While the above may be a contrived example, the significance of type guards becomes apparent when the duck type is used across multiple locations. A type guard ensures a centralized and consistent approach to control flow, serving as a single source of truth even when dealing with complex type definitions.
Exhaustive Switch
An exhaustive switch statement, also referred to as a “complete” or “total” switch, is one that covers all possible cases of a given enumeration or type. In JavaScript, which lacks built-in exhaustiveness checks, it becomes essential to establish our own standards to ensure correct usage of switch statements. To achieve this, we define a default case at the end of the switch statement that throws an error:
switch (animal) { case "Dog": return "Woof!"; case "Cat": return "Meow!"; case "Bird": return "Tweet!"; // The following line ensures the switch statement is exhaustive default: // This block will execute if there's an unhandled case throw "This animal is not implemented!"}This has the benefit that whenever an enum or type is extended and used throughout the application explicit errors will be thrown, instead of silently failing and causing hard to understand bugs.
Imports
We define globally resused imports in the tsconfig.json file under compilerOptions > paths such that we can change the following import in a deeply nested file:
import { Component } from "../../../../common"To a more readable format:
import { Component } from "@/common"File APIs
We make use of index files to make file definitions accesible to other parts of the code, acting as internal APIs. This allows for a clearer distinction of which parts of the code are used only locally, and which parts are used external to the current directory as well.
// We export everything with * as the only export should be externally available,// if there are exports which should not be externally available do not use a wildcard.export * from "./component";
// component/component.tsxexport Component = () => { return <>Hello!</>}
// otherComponent/otherComponent.tsximport { Component } from "../component";An incorrect way of accesing the component would be:
export Component = () => { return <>Hello!</>}
// otherComponent/otherComponent.tsximport { Component } from "../component/component";In the above it is no longer clear if Component is exported only to files in the local directory or also to parent or sibling directories.
Version Control
This section provides protocols and standards on version control.
Branches
The application’s codebase is organized around a primary persistent branch called main, which consistently reflects the production version of the code. In addition to the main branch, we utilize three types of ephemeral branches:
- feature: These branches are created to implement new features or modify existing functionality.
- bug: Ephemeral branches of this type are established to address one or more related bugs.
- hotfix: These branches are initiated for the purpose of making minor changes to swiftly resolve low-effort bugs.
Ephemeral branches are created anew for each distinct feature, bug, or hotfix. These branches should be deleted once the associated pull request (PR) has been completed.
PR’s
When a feature, bug or hotfix has been completed a PR should be created for a merge into the main branch. This PR should be validated by a different developer than the developer who created the PR.