DEVELOPER

TypeScript Guidelines

Overview

This guide covers design principles and best practices as observed in the typeScript code of our frontend. We use non-native web components written in TypeScript for adding behaviour to server side rendered templates. The TypeScript components are loaded dynamically via webpack as chunks. This dynamic loading behaviour is handled by services written at the centre of our application along with other base functionalities also know as core.

Based on this core we write components that can be loaded on demand when needed on the page. Components are essentially pieces of code that are bound to a certain DOM element and add behaviour to DOM in one way or another. Their life cycle starts by being loaded via a DOM element and are ideally agnostic to everything outside the DOM element they are bound to. Think of them like lego blocks that fulfil different behavioural purposes but are not dependent on other blocks directly.

Since components are supposed to be standalone, In order for multiple components to talk to each other we use services. Services are different from components for multiple reasons (discussed underneath) but most important difference is that services are agnostic to DOM structure and are singleton in nature.


General guidelines

  • External libraries/dependencies used by a class should be loaded dynamically as webpack chunks. This makes sure that multiple instances of the class on the same page don’t trigger multiple downloads of external library/dependency.
  • Use early returns wherever possible. This prevents parser from parsing extra code which can give a slight performance benefit. Additionally it makes code much more readable.
  • Getter and setter methods must be defined before constructor definition in a class.
  • Init method must only be defined if there are async calls needed, otherwise constructor should take care of all initialisations.
  • If there are cases in your code that are not handled due to any reason, it is advisable to add a log when that case is met, this ensures silent bugs from happening. You can use the pre-existing logger service for this purpose.
  • Name constants starting with their type. e.g. if there is a class selector constant, it should be named as SELECTOR_CLASS_XYZ instead of XYZ_CLASS_SELECTOR.
  • Avoid using `any` as a type at all costs, when dealing with a new type consider using TypeScript interfaces of types.
  • Abstract common behaviours in abstract classes. If you come across components or services that have some redundant code, a very simple thing you can do is to move that behaviour to an abstract class and use inheritance to add missing behaviour from specific components.
  • Overly generic tasks should be added as helpers in core. Things that are redundant and suspected to be useful in other places e.g. custom datatype conversions, hashing functions e.t.c. can be extracted out as a pure functions and put under helpers folder in core. This way other developers can make use of these.

Components guidelines

  • A component file name must always be postfixed with .component. e.g. xyz.component.ts
  • Component should be atomic, meaning it should focus on one and one task only. Composing multiple small components to achieve a bigger task is better than writing a big component doing multiple things. We do have components that handle large number of functionalists at the moment, however those might be exceptions or need refactoring at some point if possible.
  • Components should introduce minimum amount of data on their own and rely mostly on data passed to them via data attributes from the related DOM element. This means wherever possible, avoid introducing initial state in the components via constants or variable, rather pass initial state from backend using data attributes. This offers flexibility and makes components easy to reuse. Additionally it makes them easier to mock when testing.
  • A component should be agnostic to other components. This means that the only information that component should need to function should be provided to it from the associated DOM element. In case you absolutely have to reference other components in a component, make sure that the component fails gracefully when the referenced component is missing.
  • When conceptualising components, focus on behaviour and aim for generic use case rather than creating very use case specific components. This reduces reusability and makes components hard to scale. As an example lets say you want to write a component that hides a button on header. Rather than creating a component called header-button-hider.component.ts, a better approach would be to create a component called class-toggle.component.ts and pass the hidden class to it as data property. This we it can be used in other places as well.

Services guidelines

  • Services file names should end with service post fix. e.g. xyz.service.ts.
  • All services in the shop are singleton with very rare exceptions, the reason for that is the core purpose of how we use services i.e. to act as a single source of truth for multiple components. As such having multiple instances of a service does not help serve our use case.
  • Service should contain no direct references to DOM elements. If a service needs to be aware of an element, it should be injected via setter methods called from the component into that service. The purpose of services in our case is not to manipulate DOM, rather hold state and perform common tasks.
  • Use behaviourSubjects to maintain state. We use RxJS for adding reactivity to our application. behaviourSubjects are great because they have an initial state and every subscriber is destined to get a value no matter when they subscribe. As such avoid maintaining state in services without the use of behaviourSubjects.

Best practices

  • Always check build output for linter warnings and fix before pushing code. While the warnings would not prevent you from a successful build, they do pile up over time and can affect the quality of code.
  • We are trying to increase our unit tests coverage. While we don’t have any rules for test driven development, it is highly recommended to think about testing before writing the code in cases where writing the test before implementing the code is not possible. This is an easy way of ensuring good code quality.
  • Avoid direct communication between services. It is not always possible to achieve but a good practice is to use components for composing different services and pass data on demand rather than making one service aware of the other directly.