Objects and Data Structures

Use getters and setters

TypeScript supports getter/setter syntax. Using getters and setters to access data from objects that encapsulate behavior could be better than simply looking for a property on an object. "Why?" you might ask. Well, here's a list of reasons:

  • When you want to do more beyond getting an object property, you don't have to look up and change every accessor in your codebase.
  • Makes adding validation simple when doing a set.
  • Encapsulates the internal representation.
  • Easy to add logging and error handling when getting and setting.
  • You can lazy load your object's properties, let's say getting it from a server.

Bad:

type BankAccount = {
  balance: number;
  // ...
}

const value = 100;
const account: BankAccount = {
  balance: 0,
  // ...
};

if (value < 0) {
  throw new Error('Cannot set negative balance.');
}

account.balance = value;

Good:

class BankAccount {
  private accountBalance: number = 0;

  get balance(): number {
    return this.accountBalance;
  }

  set balance(value: number) {
    if (value < 0) {
      throw new Error('Cannot set negative balance.');
    }

    this.accountBalance = value;
  }

  // ...
}

// Now `BankAccount` encapsulates the validation logic.
// If one day the specifications change, and we need extra validation rule,
// we would have to alter only the `setter` implementation,
// leaving all dependent code unchanged.
const account = new BankAccount();
account.balance = 100;

Make objects have private/protected members

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

Bad:

class Circle {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

Good:

class Circle {
  constructor(private readonly radius: number) {
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

Prefer immutability

TypeScript's type system allows you to mark individual properties on an interface/class as readonly. This allows you to work in a functional way (an unexpected mutation is bad).
For more advanced scenarios there is a built-in type Readonly that takes a type T and marks all of its properties as readonly using mapped types (see mapped types).

Bad:

interface Config {
  host: string;
  port: string;
  db: string;
}

Good:

interface Config {
  readonly host: string;
  readonly port: string;
  readonly db: string;
}

For arrays, you can create a read-only array by using ReadonlyArray<T>. It doesn't allow changes such as push() and fill(), but can use features such as concat() and slice() that do not change the array's value.

Bad:

const array: number[] = [ 1, 3, 5 ];
array = []; // error
array.push(100); // array will be updated

Good:

const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // error
array.push(100); // error

Declaring read-only arguments in TypeScript 3.4 is a bit easier.

function hoge(args: readonly string[]) {
  args.push(1); // error
}

Prefer const assertions for literal values.

Bad:

const config = {
  hello: 'world'
};
config.hello = 'world'; // value is changed

const array  = [ 1, 3, 5 ];
array[0] = 10; // value is changed

// writable objects is returned
function readonlyData(value: number) {
  return { value };
}

const result = readonlyData(100);
result.value = 200; // value is changed

Good:

// read-only object
const config = {
  hello: 'world'
} as const;
config.hello = 'world'; // error

// read-only array
const array  = [ 1, 3, 5 ] as const;
array[0] = 10; // error

// You can return read-only objects
function readonlyData(value: number) {
  return { value } as const;
}

const result = readonlyData(100);
result.value = 200; // error

type vs. interface

Use type when you might need a union or intersection. Use an interface when you want extends or implements. There is no strict rule, however, use the one that works for you.
For a more detailed explanation refer to this answer about the differences between type and interface in TypeScript.

Bad:

interface EmailConfig {
  // ...
}

interface DbConfig {
  // ...
}

interface Config {
  // ...
}

//...

type Shape = {
  // ...
}

Good:


type EmailConfig = {
  // ...
}

type DbConfig = {
  // ...
}

type Config  = EmailConfig | DbConfig;

// ...

interface Shape {
  // ...
}

class Circle implements Shape {
  // ...
}

class Square implements Shape {
  // ...
}

results matching ""

    No results matching ""