Module Augmentation

Introduction

Dạo gần đây mình chủ yếu làm việc với leaflet, một thư viện js về bản đồ, và mình luôn có nhu cầu mở rộng các chức năng của thư viện này ra. Ví dụ mình muốn thêm thuộc tính và phương thức mới vào một base class có sẵn của thư viện leaflet. Và vì là base class (class này đã được sử dụng ở nhiều nơi và ta cũng không có quyền truy cập để chỉnh sửa trực tiếp) nên ta không thể giải quyết được bằng phương cách kế thừa thông qua từ khóa extends. Và chức năng mình gọi nôm na là tăng cường module (module augmentation) của TypeScript là cứu cánh của mình lúc ấy. Trước khi đi sâu vào module augmentation mình sẽ nói về 1 chức năng cơ bản hơn của TypeScript đó chính là declaration merging.

Declaration Merging

TypeScript cho phép ta merge interface với interface:

interface Box {
  height: number;
  width: number;
}
interface Box {
  scale: number;
}
let box: Box = { height: 5, width: 6, scale: 10 };

Ngoài ra ta còn có thể merge interface với class:

class Food {
  cheese: string;
}
interface Food {
  bacon: string;
}
const food  = new Food();
food.bacon = "nice bacon";
food.cheese = "sweet cheese";
console.log(food); // {bacon: "nice bacon", cheese: "sweet cheese"}

Tuy nhiên trong trường hợp interface của ta có chứa phương thức thì sẽ ra sao:

class Food {
  cheese: string;
}
interface Food {
  bacon: string;
  bake(item: string);
}
const food  = new Food();
food.bake("cake"); // Error: food.bake is not a function

Bởi vì interface của ta chỉ có thể chứa khai báo cho phương thức chứ chưa có phần implementation của phương thức nên ta chưa thể sử dụng nó. Ta có thể implement thông qua prototype của js như sau:

Food.prototype.bake = (item) => console.log(item);

Module Augmentation

Module Augmentation giúp chúng ta mở rộng chức năng của một thư viện (3rd party libraries) cái mà chúng ta không có quyền truy cập để chỉnh sửa trực tiếp. Quay trở lại với vấn đề với leaflet mà mình đã đề cập ở đầu bài. Mình sẽ sử dụng chức năng module augmentation để thêm thuộc tính zIndex và phương thức setZIndex vào class Path của thư viện leaflet như sau:
Đầu tiên ta import class Path từ thư viện leaflet:

import { Path } from 'leaflet';

Sau đó ta dùng từ khóa declare module với tên trùng với tên của leaflet module và interface với tên trùng với tên của class Path mà ta đang muốn mở rộng:

declare module 'leaflet' {
  interface Path {
    zIndex?: number;
    setZIndex(zIndex: number): this;
  }
}

Và sau cùng là thêm phần implementation cho phương thức setZIndex của ta:

Path.prototype.setZIndex = (zIndex) => console.log(zIndex);

Đến đây bạn đọc có thể sẽ thắc mắc tại sao ta không dùng class để merge cho dễ vì trong class ta có thể khai báo phương thức và cả phần implementation cho phương thức ấy. Lý do chính là TypeScript không cho phép ta merge giữa class với class. Tuy nhiên TypeScript vẫn có cung cấp cho ta 1 giải pháp mô phỏng việc này đó chính là mixins và đó cũng sẽ là chủ đề cho bài viết tiếp theo của mình.
Hãy cùng thảo luận bên dưới comment bạn nhé - Sound off on the comment...

Tài liệu tham khảo

Long Nguyễn

Mình là Long Nguyễn, một lập trình viên. Blog này ra đời nhằm mục đích ghi lại những gì mình học được, những kinh nghiệm từ thực tế, chia sẻ đến các bạn trẻ trong cùng lĩnh vực. Với quan điểm chia sẻ để học hỏi nhiều hơn, mình cũng sẽ học được nhiều điều mới từ các bạn.