TypeScript Strategies for Angular, Explaining Best Practices and Techniques

Follow on LinkedIn

Angular has fully embraced TypeScript as its primary development language, a decision that has evolved into a widely accepted standard practice. TypeScript introduces robust features for static typing and code organization.

TypeScript is not exclusive to Angular, as other popular web frameworks like React, Vue.js, and Svelte have also used its capabilities.

Typescript in Angular

In this article, we will dive into best practices and effective patterns for using TypeScript with Angular.

Crafting Classes and Types

Object-Oriented Foundations

Angular, being deeply rooted in object-oriented principles, heavily relies on classes. Let’s explore the basics of creating classes and types in TypeScript.

// Example: Crafting a fundamental class in TypeScript
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// Example: Instantiating an object from the class
export function createPerson() {
  let client: Person = new Person("Mario", 7);
  console.log(`Name:${client.name} Age:${client.age}`);
}

In this example, we define a Person class with name and age properties, followed by a constructor method to specify how objects will be instantiated from this class. The createPerson function showcases how to create and utilize an instance of the Person class.

Securing Data: Encapsulation and Inheritance

// Example: Introducing encapsulation and inheritance in a class
class Person {
  name: string;
  age: number;
  private id: number;  // Encapsulated private property

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.floor(Math.random() * 1000);
  }

  toString() {
    return `Name:${this.name} Age:${this.age} ID: ${this.id}`;
  }
}

// Example: Attempting to access a private property
export function demonstrateEncapsulation() {
  let client: Person = new Person("Mario", 7);
  console.log(client.toString());
  // The next line triggers a TypeScript error
  // client.id = 100;
}

This example introduces the concept of encapsulation by adding a private property, id, to the Person class. Trying to access this private property from outside the class results in a TypeScript error. Additionally, we delve into inheritance by creating a Client class that extends the Person class.

// Example: Inheritance in TypeScript
class Client extends Person {
  address: string;

  constructor(name: string, age: number, address: string) {
    super(name, age);
    this.address = address;
  }

  toString(): string {
    return `${super.toString()} Address: ${this.address}`;
  }
}

Here, the Client class inherits from the Person class, introducing an address property. We also override the toString method to include information from both classes.

Utilizing Interfaces

// Example: Implementing interfaces in TypeScript
export interface Animal {
  species: string;
  kingdom: string;
  class: string;
}

// Example: Utilizing an interface to create an object
export function createAnimal() {
  let chicken: Animal = {
    kingdom: "Animalia",
    species: "Gallus",
    class: "birds",
  };
  console.log(`Kingdom:${chicken.kingdom} Species:${chicken.species} Class:${chicken.class}`);
}

This example introduces interfaces as a means to define the structure of an object. The Animal interface represents properties common to different types of animals, and the createAnimal function demonstrates how to use this interface to create an object.

Simplifying with Type Aliases

// Example: Leveraging type aliases in TypeScript
type Machine = {
  id: number;
  description: string;
  energyOutput: number;
};

export function createMachine() {
  let car: Machine = {
    id: 123,
    description: "Car",
    energyOutput: 1000,
  };
  console.log(`ID:${car.id} Description:${car.description} Energy Output:${car.energyOutput} `);
}

Type aliases offer a more straightforward way to create types. In this example, we create a Machine type using a type alias and use it to declare a variable. Additionally, we explore creating type aliases from other types.

Choosing Between Classes, Interfaces, and Types

When deciding which approach to use, consider the following guidelines:

Type Alias: Use for simple variable typing, especially for function parameters and return types.

Interface: Ideal for representing JSON data objects, where methods are not present. Also used to define contracts for classes using the implements keyword.

Class: The foundation of object-oriented programming, suitable for creating objects with both methods and attributes. In Angular, components, and services are ultimately objects created from classes.

Streamlining Code: Type Inference

// Example: Harnessing type inference in TypeScript
export function inferTypes() {
  let name = "Mario";
  let age = 9;
  let isAlive = true;
  console.log(`Name:${name} Age:${age} is alive:${isAlive ? "yes" : "no"}`);
}

TypeScript’s robust inference mechanisms enable variable typing without explicit declarations. In this example, variables are initialized without specifying types, and TypeScript infers the types based on the assigned values.

Ensuring Type Safety: Type Guards

Leveraging ‘typeof’ for Primitive Types

// Example: Using 'typeof' for type validation
export function basicTypeValidation() {
  let value: any = "Hello, TypeScript!";
  if (typeof value === "string") {
    console.log("This is a string");
  } else {
    console.log("This is not a string");
  }
}

The ‘typeof’ operator helps check the type of a variable at runtime. In this example, we use ‘typeof’ to determine if the value is a string.

Crafting Custom Type Guards

// Example: Creating custom type guards
export function customTypeGuard() {
  function isPerson(obj: any): obj is Person {
    return obj instanceof Person;
  }

  let user: Person | Animal = new Person("John", 30);
  if (isPerson(user)) {
    console.log("This is a Person");
  } else {
    console.log("This is not a Person");
  }
}

Custom-type guards enable defining more intricate type checks. The isPerson function in this example checks if an object is an instance of the Person class.

Discriminated Unions

// Example: Utilizing discriminated unions for type narrowing
export function discriminatedUnionExample() {
  type Circle = { kind: "circle"; radius: number };
  type Square = { kind: "square"; sideLength: number };
  type Shape = Circle | Square;

  function getArea(shape: Shape): number {
    switch (shape.kind) {
      case "circle":
        return Math.PI * shape.radius ** 2;
      case "square":
        return shape.sideLength ** 2;
    }
  }

  let circle: Circle = { kind: "circle", radius: 5 };
  let square: Square = { kind: "square", sideLength: 4 };

  consolesole.log(`Circle area: ${getArea(circle)}`);
  console.log(`Square area: ${getArea(square)}`);
}

Discriminated unions, employing a common property with literal types, enable type narrowing in TypeScript. In this example, the Shape type is a union of Circle and Square, and the kind property is used to narrow down the type in the getArea function.

Type Assertion in Typescript

// Example: Utilizing type assertion in TypeScript
export function typeAssertionExample() {
  let value: any = "Hello, TypeScript!";
  let length: number = (value as string).length;
  console.log(`Length of the string: ${length}`);
}

Type assertion provides a way to inform the compiler that a value is of a specific type. In this example, we use type assertion to treat the value as a string and access its length property.

Opting for a Safer Alternative to the ‘any’ Type

Introducing the ‘unknown’ Type

// Example: Using the 'unknown' type in TypeScript
export function unknownTypeExample() {
  let userInput: unknown;
  let userName: string;

  userInput = 5;
  if (typeof userInput === "string") {
    userName = userInput; // TypeScript error: Type 'unknown' is not assignable to type 'string'.
  } else {
    userName = "Default";
  }

  console.log(`User Name: ${userName}`);
}

The ‘unknown’ type serves as a more secure alternative to ‘any’ when the type of a variable is uncertain. In this example, userInput is declared as unknown, and type checks are performed before assigning its value to userName.

Implementing Type Guarding with ‘unknown’

// Example: Implementing type guarding with 'unknown'
export function typeGuardUnknownExample() {
  function isString(value: unknown): value is string {
    return typeof value === "string";
  }

  let userInput: unknown = "Hello, TypeScript!";
  let userName: string;

  if (isString(userInput)) {
    userName = userInput;
  } else {
    userName = "Default";
  }

  console.log(`User Name: ${userName}`);
}

Type guarding with the ‘unknown’ type involves creating custom type guards. In this example, the isString function checks if the value is a string before assigning it to userName.

Conclusion

TypeScript patterns are pivotal for crafting maintainable and scalable Angular applications. This article has covered fundamental concepts, including creating classes and types, harnessing type inference, validating types with guards, and opting for the ‘unknown’ type as a more reliable alternative to ‘any’. By implementing these patterns and adhering to best practices, you’ll elevate the robustness and clarity of your Angular codebase.

This article on Angular design patterns and best practices was curated with reference to the book “Angular Design Patterns and Best Practices” by Alvaro Camillo Neto. For a comprehensive exploration of Angular development principles, we highly recommend checking out the book. You can find it on Amazon

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

×