Skip to content

Instantly share code, notes, and snippets.

@anichitiandreea
Last active October 16, 2024 10:37
Show Gist options
  • Save anichitiandreea/e1d466022d772ea22db56399a7af576b to your computer and use it in GitHub Desktop.
Save anichitiandreea/e1d466022d772ea22db56399a7af576b to your computer and use it in GitHub Desktop.
TypeScript Code Conventions

TypeScript coding

Table of contents

Typescript coding style guide

Naming

The name of a variable, function, or class, should answer all the big questions. It should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.

Use meaningful variable names.

Distinguish names in such a way that the reader knows what the differences offer.

Bad:

function isBetween(a1: number, a2: number, a3: number): boolean {
  return a2 <= a1 && a1 <= a3;
}

Good:

 function isBetween(value: number, left: number, right: number): boolean {
   return left <= value && value <= right;
 }

Use pronounceable variable names

If you can't pronounce it, you can't discuss it without sounding weird.

Bad:

class Subs {
  public ccId: number;
  public billingAddrId: number;
  public shippingAddrId: number;
}

Good:

class Subscription {
  public creditCardId: number;
  public billingAddressId: number;
  public shippingAddressId: number;
}

Avoid mental mapping

Explicit is better than implicit.
Clarity is king.

Bad:

const u = getUser();
const s = getSubscription();
const t = charge(u, s);

Good:

const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);

Don't add unneeded context

If your class/type/object name tells you something, don't repeat that in your variable name.

Bad:

type Car = {
  carMake: string;
  carModel: string;
  carColor: string;
}

function print(car: Car): void {
  console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}

Good:

type Car = {
  make: string;
  model: string;
  color: string;
}

function print(car: Car): void {
  console.log(`${car.make} ${car.model} (${car.color})`);
}

Naming Conventions

  • Use camelCase for variable and function names

Bad:

var FooVar;
function BarFunc() { }

Good:

var fooVar;
function barFunc() { }
  • Use camelCase of class members, interface members, methods and methods parameters

Bad:

class Foo {
  Bar: number;
  Baz() { }
}

Good:

class Foo {
  bar: number;
  baz() { }
}
  • Use PascalCase for class names and interface names.

Bad:

class foo { }

Good:

class Foo { }
  • Use PascalCase for enums and camelCase for enum members

Bad:

enum notificationTypes {
  Default = 0,
  Info = 1,
  Success = 2,
  Error = 3,
  Warning = 4
}

Good:

enum NotificationTypes {
  default = 0,
  info = 1,
  success = 2,
  error = 3,
  warning = 4
}

Naming Booleans

  • Don't use negative names for boolean variables.

Bad:

const isNotEnabled = true;

Good:

const isEnabled = false;
  • A prefix like is, are, or has helps every developer to distinguish a boolean from another variable by just looking at it

Bad:

const enabled = false;

Good:

const isEnabled = false;

Brackets

The one true brace style is one of the most common brace styles in TypeScript, in which the opening brace of a block is placed on the same line as its corresponding statement or declaration.

if (foo) {
  bar();
}
else {
  baz();
}
  • Do not omit curly brackets

  • Always wrap the body of the statement in curly brackets.

Spaces

Use 2 spaces. Not tabs.

Semicolons

Use semicolons.

Code Comments

So when you find yourself in a position where you need to write a comment, think it through and see whether there isn't some way to turn the tables and express yourself in code. Every time you express yourself in code, you should pat yourself on the back. Everytime you write a comment, you should grimace and feel the failure of your ability of expression.

Bad Comments

Most comments fall into this category. Usually they are crutches or excuses for poor code or justifications for insufficient decisions, amounting to little more than the programmer talking to himself.

Mumbling

Plopping in a comment just because you feel you should or because the process requires it, is a hack. If you decide to write a comment, then spend the time necessary to make sure it is the best comment you can write.

Noise Comments

Sometimes you see comments that are nothing but noise. They restate the obvious and provide no new information.

// redirect to the Contact Details screen
this.router.navigateByUrl(`/${ROOT}/contact`);
// self explanatory, parse ...
this.parseProducts(products);

Scary noise

/** The name. */
private name;

/** The version. */
private version;

/** The licenceName. */
private licenceName;

/** The version. */
private info;

Read these comments again more carefully. Do you see the cut-paste error? If authors aren't paying attention when comments are written (or pasted), why should readers be expected to profit from them?

TODO Comments

In general, TODO comments are a big risk. We may see something that we want to do later so we drop a quick // TODO: Replace this method thinking we'll come back to it but never do.

If you're going to write a TODO comment, you should link to your external issue tracker.

There are valid use cases for a TODO comment. Perhaps you're working on a big feature and you want to make a pull request that only fixes part of it. You also want to call out some refactoring that still needs to be done, but that you'll fix in another PR.

// TODO: Consolidate both of these classes. PURCHASE-123

This is actionable because it forces us to go to our issue tracker and create a ticket. That is less likely to get lost than a code comment that will potentially never be seen again.

Comments can sometimes be useful

  • When explaining why something is being implemented in a particular way.
  • When explaining complex algorithms (when all other methods for simplifying the algorithm have been tried and come up short).

Comment conventions

  • Write comments in English.

  • Do not add empty comments

  • Begin single-line comments with a single space

Good:

// Single-line comment

Bad:

//Single-line comment
//  Single-line comment
  • Write single-line comments properly

    • Single-line comments should always be preceded by a single blank line.
    • Single-line comments should never be followed by blank line(s).

Good:

const x;

// This comment is valid
const y;

Bad:

const x;

// This comment is not valid

const y;
const x;
// This comment is not valid

const y;
  • Do not write embedded comments

    • Do not write comments between declaration of statement and opening curly brackets.
    • Place comments above statements, or within statement body.

Good:

// This method does something..
public method() {
}

Bad:

public method() { // This method does something..
}
public method() {
// This method does something..
}

Barrels

A barrel is a way to rollup exports from several modules into a single convenience module. The barrel itself is a module file that re-exports selected exports of other modules.

import noise - this is an issue seen in languages where there are dependencies that need to be "imported", "required", or "included" and the first (1 - n) lines are non functional code.

Example of a barrel file:

export * from './product-added-dialog.component';
export * from './website-selector.component';
export * from './product-family-selector.component';
export * from './individual-product-selector.component';
export * from './license-type-selector.component';
export * from './period-and-quantity-selector.component';

How to use it inside components:

Good:

import { CartsService, PaidSupportService, SettingsService } from '@modules/services';

Bad:

import { SettingsService } from './settings/settings.service';
import { CartsService } from './carts/carts.service';
import { PaidSupportService } from './paid-support/paid-support.service';
  • Barrel files are named index.ts by convention
  • Do not import a barrel in the files that are already used in that barrel because this leads to circular dependency

Angular coding style guide

Organize imports

With clean and easy to read import statements you can quickly see the dependencies of current code. Make sure you apply following good practices for import statements:

  • Unused imports should be removed.
  • Groups of imports are delineated by one blank line before and after.
  • Groups must respect following order:
    • Angular imports (i.e. import { HttpClient } from '@angular/common/http')
    • Angular material imports (i.e. import { MatSelectChange } from '@angular/material/select')
    • 3rd party imports except rxjs (i.e. import { SessionStorageService } from 'ngx-webstorage')
    • rxjs imports (i.e import { skipWhile } from 'rxjs/operators')
    • application imports sorted by type (services, classes, interfaces, enums)

Bad:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatSelectChange } from '@angular/material/select';
import { SessionStorageService } from 'ngx-webstorage';

import { merge, Observable, BehaviorSubject } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { INumberToTypeDictionary, Utils } from '@shared/classes';

import { ProductUtils } from '@modules/services/products/classes';

import { AdditionalServicesApi } from './additional-services-api';

Good:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatSelectChange } from '@angular/material/select';
import { SessionStorageService } from 'ngx-webstorage';

import { merge, Observable, BehaviorSubject } from 'rxjs';
import { filter, tap } from 'rxjs/operators';

import { INumberToTypeDictionary, Utils } from '@shared/classes';
import { ProductUtils } from '@modules/services/products/classes';
import { AdditionalServicesApi } from './additional-services-api';

Use typescript aliases

This will avoid long relative paths when doing imports.

Bad:

import { UserService } from '../../../services/UserService';

Good:

import { UserService } from '@services/UserService';

Specify component member accessor explicitly

TypeScript supports public (default), protected and private accessors on class members.

Bad:

export class ConsultingEntryComponent {
  quantities: number[] = [];
  isBeingRemoved = false;

  destroyed$ = new Subject<void>();

  constructor(private productsService: ProductsService) { }

  get isBeingProcessed(): boolean {
    return this.disabled || this.isBeingRemoved;
  }

  get isDiscountedPriceAvailable(): boolean {
    return !(Utils.isNullOrUndefined(this.fullPrice) || Utils.isNullOrUndefined(this.discounts));
  }

  onPeriodSelectionChanged(event: MatSelectChange): void {
    this.changeMade.next();
  }
}

Good:

export class ConsultingEntryComponent {
  public quantities: number[] = [];
  public isBeingRemoved = false;

  private destroyed$ = new Subject<void>();

  constructor(private productsService: ProductsService) { }

  public get isBeingProcessed(): boolean {
    return this.disabled || this.isBeingRemoved;
  }

  private get isDiscountedPriceAvailable(): boolean {
    return !(Utils.isNullOrUndefined(this.fullPrice) || Utils.isNullOrUndefined(this.discounts));
  }

  public onPeriodSelectionChanged(event: MatSelectChange): void {
    this.changeMade.next();
  }
}

Use the private or protected accessor as much as you can because it provides a better encapsulation.

Component structure

Use the following component structure:

  1. Input properties (i.e. @Input() product: OrderItemModel)
  2. Output properties (i.e. @Output() changeMade = new EventEmitter(true))
  3. ViewChild / ViewChildren (i.e. @ViewChild(ChildDirective) child!: ChildDirective)
  4. HostBinding properties (i.e. @HostBinding('class.valid') get valid() { return this.control.valid; })
  5. data members (i.e. public isBeingRemoved = false)
  6. constructor
  7. lifecycle hooks (following their execution order)
  8. getters/setters
  9. event handlers
  10. other methods

Use the following component accessors order:

  1. private
  2. protected
  3. public
  • Separate each group with a whitespace before and after

Barrels

Barrels can cause circular dependencies when they are used to import stuff from the same module. Given the structure:

module/
|- a.component.ts
|- b.component.ts
|- b.model.ts
|- index.ts

index.ts

export * from 'b.component.ts';
export * from 'a.component.ts';
export * from 'b.model.ts';

Trying to import b.component.ts inside a.component.ts through index.ts will cause a circular dependency. To solve this issue we have to import b.component.ts directly.

private Subject, public Observable pattern

Utilizing a private Subject and a public Observable allows us to lock down access to our Subject and prevent its modification.

The goal is to emit changes only from the service that the Subject belongs to and make the value emitted available through an observable. This allows us to have a SSOT (single source of truth).

Bad:

export class AdditionalServicesService extends AdditionalServicesApi {
  public escrowCartEntries$ = new BehaviorSubject<OrderItemModel[]>([]);
}
  • The Subject is made public so it can be changed outside the service.

Good:

export class AdditionalServicesService extends AdditionalServicesApi {
  public escrowCartEntries$: Observable<OrderItemModel[]>;

  private escrowCartEntriesSubject$ = new BehaviorSubject<OrderItemModel[]>([]);
  
  constructor() {
    this.escrowCartEntries$ = this.escrowCartEntriesSubject$.asObservable();
  }
}
  • Here we utilize a Subject and an Observable derived from the Subject. We emit changes to the Subject from the service, and expose it to the outside world through the Observable

Services inside HTML templates

Avoid referencing services in HTML templates.

Bad:

<div class="price">pricingService.articlePrice</div>

Good:

<div class="price">articlePrice</div>

Why to avoid it?

  • It couples the service implementation with the HTML template
  • It breaks the LoD (Law of Demeter)
  • VS Code does not support it (cannot find service reference inside template) so it makes refactoring not that fun anymore

Manage Subscriptions Declaratively

It can become a little bit tedious to make sure everything gets unsubscribed when the component is destroyed.

So the solution is to compose our subscriptions with the takeUntil operator and use a subject that emits a truthy value in the ngOnDestroy lifecycle hook.

Bad:

import { Component, OnDestroy } from '@angular/core';

@Component({ ... })
export class AppComponent implements OnDestroy {
  subject$ = new Subject<void>();

  constructor() {}

  ngOnDestroy() {
    this.subject$.unsubscribe();
  }
}

Good:

import { Component, OnDestroy } from '@angular/core';

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({ ... })
export class AppComponent implements OnDestroy {
  rxSubscriptionCancellationSubject$ = new Subject<void>();

  constructor(private downloadsService: DownloadsService) {
    this.downloadsService.getRenewalQuotationAsFile$
      .pipe(
        takeUntil(this.rxSubscriptionCancellationSubject$)
      )
      .subscribe(
        result => {
          console.log(`${ID}: getRenewalQuotationAsFile$ emitted`, result);
        }
      );
  }

  ngOnDestroy(): void {
    this.rxSubscriptionCancellationSubject$.next();
    this.rxSubscriptionCancellationSubject$.complete();
  }
}
Copy link

ghost commented Feb 2, 2023

congratulations this is beautiful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment