+2

11 khái niệm TypeScript nâng cao mọi lập trình viên nên biết

TypeScript là một ngôn ngữ lập trình hiện đại thường được ưa chuộng hơn JavaScript nhờ tính an toàn kiểu. Bài viết này sẽ chia sẻ 10 khái niệm TypeScript hàng đầu giúp bạn nâng cao kỹ năng lập trình TypeScript.

1. Generics

TypeScript là một ngôn ngữ lập trình hiện đại thường được ưa chuộng hơn JavaScript nhờ tính an toàn kiểu. Trong bài viết này, tôi sẽ chia sẻ 10 khái niệm TypeScript hàng đầu sẽ giúp bạn nâng cao kỹ năng lập trình TypeScript. Bạn đã sẵn sàng chưa? Hãy bắt đầu nào.

Sử dụng generics, chúng ta có thể tạo các kiểu tái sử dụng. Điều này sẽ hữu ích trong việc xử lý dữ liệu của hiện tại cũng như tương lai.

Ví dụ về Generics:

Chúng ta có thể muốn một hàm trong TypeScript nhận một đối số là một kiểu nào đó và trả về cùng kiểu đó.

function func<T>(args:T):T{
    return args;
}

2. Generics với ràng buộc kiểu

Sử dụng ràng buộc kiểu, chúng ta có thể giới hạn kiểu T bằng cách định nghĩa nó chỉ chấp nhận chuỗi và số nguyên.

function func<T extends string | number>(value: T): T {
    return value;
}

const stringValue = func("Hello"); // Works, T is string
const numberValue = func(42);      // Works, T is number

// const booleanValue = func(true); // Error: Type 'boolean' is not assignable to type 'string | number'

3. Giao diện generics

Giao diện generics rất hữu ích khi bạn muốn định nghĩa các hợp đồng (hình dạng) cho các đối tượng, lớp hoặc hàm hoạt động với nhiều kiểu khác nhau. Chúng cho phép bạn định nghĩa một bản thiết kế có thể thích ứng với các kiểu dữ liệu khác nhau trong khi vẫn giữ nguyên cấu trúc.

// Generic interface with type parameters T and U
interface Repository<T, U> {
    items: T[];           // Array of items of type T
    add(item: T): void;   // Function to add an item of type T
    getById(id: U): T | undefined; // Function to get an item by ID of type U
}

// Implementing the Repository interface for a User entity
interface User {
    id: number;
    name: string;
}

class UserRepository implements Repository<User, number> {
    items: User[] = [];

    add(item: User): void {
        this.items.push(item);
    }

     getById(idOrName: number | string): User | undefined {
        if (typeof idOrName === 'string') {
            // Search by name if idOrName is a string
            console.log('Searching by name:', idOrName);
            return this.items.find(user => user.name === idOrName);
        } else if (typeof idOrName === 'number') {
            // Search by id if idOrName is a number
            console.log('Searching by id:', idOrName);
            return this.items.find(user => user.id === idOrName);
        }
        return undefined; // Return undefined if no match found
    }
}

// Usage
const userRepo = new UserRepository();
userRepo.add({ id: 1, name: "Alice" });
userRepo.add({ id: 2, name: "Bob" });

const user1 = userRepo.getById(1);
const user2 = userRepo.getById("Bob");
console.log(user1); // Output: { id: 1, name: "Alice" }
console.log(user2); // Output: { id: 2, name: "Bob" }

4. Generic Classes

Hãy sử dụng Generic Classes khi bạn muốn tất cả các thuộc tính trong lớp của mình tuân theo kiểu được chỉ định bởi tham số generic. Điều này cho phép tính linh hoạt trong khi vẫn đảm bảo rằng mọi thuộc tính của lớp khớp với kiểu được truyền cho lớp.

interface User {
    id: number;
    name: string;
    age: number;
}

class UserDetails<T extends User> {
    id: T['id'];
    name: T['name'];
    age: T['age'];

    constructor(user: T) {
        this.id = user.id;
        this.name = user.name;
        this.age = user.age;
    }

    // Method to get user details
    getUserDetails(): string {
        return `User: ${this.name}, ID: ${this.id}, Age: ${this.age}`;
    }

    // Method to update user name
    updateName(newName: string): void {
        this.name = newName;
    }

    // Method to update user age
    updateAge(newAge: number): void {
        this.age = newAge;
    }
}

// Using the UserDetails class with a User type
const user: User = { id: 1, name: "Alice", age: 30 };
const userDetails = new UserDetails(user);

console.log(userDetails.getUserDetails());  // Output: "User: Alice, ID: 1, Age: 30"

// Updating user details
userDetails.updateName("Bob");
userDetails.updateAge(35);

console.log(userDetails.getUserDetails());  // Output: "User: Bob, ID: 1, Age: 35"
console.log(new UserDetails("30"));  // Error: "This will throw error" 

5. Ràng buộc tham số kiểu

Đôi khi, chúng ta muốn một kiểu tham số phụ thuộc vào một số tham số được truyền khác. Nghe có vẻ khó hiểu, hãy xem ví dụ dưới đây.

function getProperty<Type>(obj: Type, key: keyof Type) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a");  // Valid
getProperty(x, "d");  // Error: Argument of type '"d"' is not assignable to parameter of type '"a" | "b" | "c"'.

6. Kiểu điều kiện

Thông thường, chúng ta muốn các kiểu của mình là kiểu này hoặc kiểu khác. Trong những trường hợp như vậy, chúng ta sử dụng kiểu có điều kiện.

Một ví dụ đơn giản:

function func(param:number|boolean){
return param;
}
console.log(func(2)) //Output: 2 will be printed
console.log(func("True")) //Error: boolean cannot be passed as argument

Một ví dụ phức tạp hơn một chút:

type HasProperty<T, K extends keyof T> = K extends "age" ? "Has Age" : "Has Name";

interface User {
  name: string;
  age: number;
}

let test1: HasProperty<User, "age">;  // "Has Age"
let test2: HasProperty<User, "name">; // "Has Name"
let test3: HasProperty<User, "email">; // Error: Type '"email"' is not assignable to parameter of type '"age" | "name"'.

7. Kiểu giao hội

Kiểu giao hội rất hữu ích khi chúng ta muốn kết hợp nhiều kiểu thành một, cho phép một kiểu cụ thể kế thừa các thuộc tính và hành vi từ nhiều kiểu khác nhau.

Hãy xem một ví dụ thú vị:

// Defining the types for each area of well-being

interface MentalWellness {
  mindfulnessPractice: boolean;
  stressLevel: number; // Scale of 1 to 10
}

interface PhysicalWellness {
  exerciseFrequency: string; // e.g., "daily", "weekly"
  sleepDuration: number; // in hours
}

interface Productivity {
  tasksCompleted: number;
  focusLevel: number; // Scale of 1 to 10
}

// Combining all three areas into a single type using intersection types
type HealthyBody = MentalWellness & PhysicalWellness & Productivity;

// Example of a person with a balanced healthy body
const person: HealthyBody = {
  mindfulnessPractice: true,
  stressLevel: 4,
  exerciseFrequency: "daily",
  sleepDuration: 7,
  tasksCompleted: 15,
  focusLevel: 8
};

// Displaying the information
console.log(person);

8. Từ khóa infer

Từ khóa infer hữu ích khi chúng ta muốn xác định có điều kiện một kiểu cụ thể và khi điều kiện được đáp ứng, nó cho phép chúng ta trích xuất các kiểu con từ kiểu đó.

Cú pháp chung:

type ConditionalType<T> = T extends SomeType ? InferredType : OtherType;

Ví dụ:

type ReturnTypeOfPromise<T> = T extends Promise<infer U> ? U : number;

type Result = ReturnTypeOfPromise<Promise<string>>;  // Result is 'string'
type ErrorResult = ReturnTypeOfPromise<number>;      // ErrorResult is 'never'

const result: Result = "Hello";
console.log(typeof result); // Output: 'string'

9. Type Variance

Khái niệm Phương sai kiểu nói về cách kiểu con và kiểu cha liên quan đến nhau. Có hai loại:

  • Hiệp biến (Covariance): Kiểu con có thể được sử dụng ở nơi kiểu cha được mong đợi.

VD:

class Vehicle { }
class Car extends Vehicle { }

function getCar(): Vehicle {
  return new Car();
}

function getVehicle(): Vehicle {
  return new Vehicle();
}

// Covariant assignment
let car: Car = getCar();
let vehicle: Vehicle = getVehicle(); // This works because Car is a subtype of Vehicle

Trong ví dụ trên, Car đã kế thừa các thuộc tính từ lớp Vehicle, vì vậy hoàn toàn hợp lệ khi gán nó cho kiểu con ở nơi kiểu cha được mong đợi vì kiểu con sẽ có tất cả các thuộc tính mà kiểu cha có.

  • Ngược biến (Contravariance): Điều này ngược lại với hiệp biến. Chúng ta sử dụng kiểu cha ở những nơi kiểu con được mong đợi.
class Vehicle {
  startEngine() {
    console.log("Vehicle engine starts");
  }
}

class Car extends Vehicle {
  honk() {
    console.log("Car honks");
  }
}

function processVehicle(vehicle: Vehicle) {
  vehicle.startEngine(); // This works
  // vehicle.honk(); // Error: 'honk' does not exist on type 'Vehicle'
}

function processCar(car: Car) {
  car.startEngine(); // Works because Car extends Vehicle
  car.honk();        // Works because 'Car' has 'honk'
}

let car: Car = new Car();
processVehicle(car); // This works because of contravariance (Car can be used as Vehicle)
processCar(car);     // This works as well because car is of type Car

// Contravariance failure if you expect specific subtype behavior in the method

Khi sử dụng ngược biến, chúng ta cần thận trọng không truy cập các thuộc tính hoặc phương thức dành riêng cho kiểu con, vì điều này có thể dẫn đến lỗi.

10. Reflections

Phản xạ liên quan đến việc xác định kiểu của một biến tại thời điểm chạy. Mặc dù TypeScript chủ yếu tập trung vào việc kiểm tra kiểu tại thời điểm biên dịch, chúng ta vẫn có thể tận dụng các toán tử TypeScript để kiểm tra kiểu trong thời điểm chạy.

  • Toán tử typeof: Chúng ta có thể sử dụng toán tử typeof để tìm kiểu của biến tại thời điểm chạy.
const num = 23;
console.log(typeof num); // "number"

const flag = true;
console.log(typeof flag); // "boolean"
  • Toán tử instanceof: Toán tử instanceof có thể được sử dụng để kiểm tra xem một đối tượng có phải là một thể hiện của một lớp hoặc một kiểu cụ thể hay không.
class Vehicle {
  model: string;
  constructor(model: string) {
    this.model = model;
  }
}

const benz = new Vehicle("Mercedes-Benz");
console.log(benz instanceof Vehicle); // true

Chúng ta có thể sử dụng thư viện của bên thứ ba để xác định kiểu tại thời điểm chạy.

11. Dependency Injection

Dependency Injection là một mẫu cho phép bạn đưa mã vào thành phần của mình mà không cần thực sự tạo hoặc quản lý nó ở đó. Mặc dù nó có vẻ giống như sử dụng thư viện, nhưng nó khác vì bạn không cần cài đặt hoặc nhập nó thông qua CDN hoặc API.

Thoạt nhìn, nó cũng có vẻ giống với việc sử dụng các hàm để tái sử dụng, vì cả hai đều cho phép tái sử dụng mã. Tuy nhiên, nếu chúng ta sử dụng các hàm trực tiếp trong các thành phần của mình, nó có thể dẫn đến sự kết hợp chặt chẽ giữa chúng.

Điều này có nghĩa là bất kỳ thay đổi nào trong hàm hoặc logic của nó có thể ảnh hưởng đến mọi nơi nó được sử dụng. Dependency Injection giải quyết vấn đề này bằng cách tách việc tạo các phụ thuộc khỏi các thành phần sử dụng chúng, làm cho mã dễ bảo trì và kiểm tra hơn.

Ví dụ không có dependency injection:

// Health-related service classes without interfaces
class MentalWellness {
  getMentalWellnessAdvice(): string {
    return "Take time to meditate and relax your mind.";
  }
}

class PhysicalWellness {
  getPhysicalWellnessAdvice(): string {
    return "Make sure to exercise daily for at least 30 minutes.";
  }
}

// HealthAdvice class directly creating instances of the services
class HealthAdvice {
  private mentalWellnessService: MentalWellness;
  private physicalWellnessService: PhysicalWellness;

  // Directly creating instances inside the class constructor
  constructor() {
    this.mentalWellnessService = new MentalWellness();
    this.physicalWellnessService = new PhysicalWellness();
  }

  // Method to get both mental and physical wellness advice
  getHealthAdvice(): string {
    return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
  }
}

// Creating an instance of HealthAdvice, which itself creates instances of the services
const healthAdvice = new HealthAdvice();

console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at least 30 minutes."

Ví dụ với Dependecy Injection:

// Health-related service interfaces with "I" prefix
interface IMentalWellnessService {
  getMentalWellnessAdvice(): string;
}

interface IPhysicalWellnessService {
  getPhysicalWellnessAdvice(): string;
}

// Implementations of the services
class MentalWellness implements IMentalWellnessService {
  getMentalWellnessAdvice(): string {
    return "Take time to meditate and relax your mind.";
  }
}

class PhysicalWellness implements IPhysicalWellnessService {
  getPhysicalWellnessAdvice(): string {
    return "Make sure to exercise daily for at least 30 minutes.";
  }
}

// HealthAdvice class that depends on services via interfaces
class HealthAdvice {
  private mentalWellnessService: IMentalWellnessService;
  private physicalWellnessService: IPhysicalWellnessService;

  // Dependency injection via constructor
  constructor(
    mentalWellnessService: IMentalWellnessService,
    physicalWellnessService: IPhysicalWellnessService
  ) {
    this.mentalWellnessService = mentalWellnessService;
    this.physicalWellnessService = physicalWellnessService;
  }

  // Method to get both mental and physical wellness advice
  getHealthAdvice(): string {
    return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
  }
}

// Dependency injection
const mentalWellness: IMentalWellnessService = new MentalWellness();
const physicalWellness: IPhysicalWellnessService = new PhysicalWellness();

// Injecting services into the HealthAdvice class
const healthAdvice = new HealthAdvice(mentalWellness, physicalWellness);

console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at least 30 minutes."

Trong một kịch bản kết hợp chặt chẽ, nếu bạn có một thuộc tính stressLevel trong lớp MentalWellness hôm nay và quyết định thay đổi nó thành một cái gì đó khác vào ngày mai, bạn sẽ cần cập nhật tất cả những nơi nó được sử dụng. Điều này có thể dẫn đến rất nhiều thách thức về tái cấu trúc và bảo trì.

Tuy nhiên, với Dependency Injection và việc sử dụng giao diện, bạn có thể tránh được vấn đề này. Bằng cách truyền các phụ thuộc (chẳng hạn như dịch vụ MentalWellness) thông qua hàm tạo, các chi tiết triển khai cụ thể (như thuộc tính stressLevel) được trừu tượng hóa đằng sau giao diện.

Điều này có nghĩa là những thay đổi đối với thuộc tính hoặc lớp không yêu cầu sửa đổi trong các lớp phụ thuộc, miễn là giao diện vẫn giữ nguyên. Cách tiếp cận này đảm bảo rằng mã được kết hợp lỏng lẻo, dễ bảo trì hơn và dễ kiểm tra hơn, vì bạn đang tiêm những gì cần thiết tại thời điểm chạy mà không cần kết hợp chặt chẽ các thành phần.

Cảm ơn các bạn đã theo dõi!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí