0

Series Total TypeScript Thực chiến #9: Module Augmentation – "Hack" Type của thư viện

1. Nỗi đau khi sử dụng Middleware (Ví dụ: Express.js)

Kịch bản kinh điển nhất trong Backend là viết một Middleware xác thực (Authentication). Khi người dùng gửi Request lên, Middleware sẽ giải mã Token, lấy ra thông tin user và gắn ngược lại vào object req để các Controller phía sau sử dụng.

import express, { Request, Response, NextFunction } from 'express';

const app = express();

// Middleware xác thực
function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization;
  // Giả sử giải mã token thành công ra cục data này:
  const userData = { id: 'U01', role: 'admin' };

  // ❌ LỖI ĐỎ LÒM TẠI ĐÂY
  req.user = userData; 
  // TypeScript phàn nàn: Property 'user' does not exist on type 'Request'.
  
  next();
}

Cách giải quyết tồi tệ nhất mà nhiều người sử dụng là ép kiểu về any: (req as any).user = userData. Hành động này phá nát hoàn toàn lớp giáp bảo vệ mà chúng ta đã cất công xây dựng từ đầu series.

2. Nguyên lý "Declaration Merging" (Gộp khai báo)

TypeScript có một tính năng rất đặc biệt gọi là Declaration Merging. Nếu bạn khai báo hai interface có cùng một tên trong cùng một không gian (scope), TypeScript sẽ tự động hòa trộn các thuộc tính của chúng lại làm một thay vì báo lỗi trùng lặp.

interface Car { brand: string; }
interface Car { price: number; }

// Lúc này, type Car thực tế sẽ có cả brand và price
const myCar: Car = { brand: 'Toyota', price: 1000 };

Module Augmentation chính là việc lợi dụng cơ chế này để chui vào tận "nhà" của thư viện bên thứ 3, khai báo lại cái interface của họ và nhét thêm đồ đạc của mình vào.

3. Thực chiến: Hack Type của Express.js

Để mở rộng Type cho một module bên ngoài, chúng ta sử dụng từ khóa declare module "tên-thư-viện".

Bạn có thể viết đoạn code này vào một file định nghĩa riêng (ví dụ: types/express/index.d.ts) hoặc viết thẳng lên đầu file cấu hình hệ thống:

import { Request } from 'express';

// Định nghĩa cấu trúc User của riêng hệ thống bạn
interface CustomUser {
  id: string;
  role: string;
}

// 🔥 BẮT ĐẦU HACK (Module Augmentation)
declare global {
  namespace Express {
    // Ghi đè/Mở rộng interface Request có sẵn của Express
    interface Request {
      user?: CustomUser; // Thêm dấu ? vì không phải API nào cũng đi qua Auth
    }
  }
}

(Lưu ý: Cú pháp chính xác để mở rộng Express thường nằm trong namespace Express bên trong declare global, hoặc declare module 'express-serve-static-core').

Bây giờ, hãy quay lại file Middleware của chúng ta:

function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const userData = { id: 'U01', role: 'admin' };

  // ✅ Ngon lành! TypeScript không còn báo lỗi nữa.
  req.user = userData; 
  next();
}

app.get('/profile', (req, res) => {
  // ✅ Rê chuột vào req.user, IDE sẽ gợi ý chuẩn xác id và role
  console.log(`Xin chào user có ID: ${req.user?.id}`);
});

4. Mở rộng Global Scope (Biến môi trường)

Không chỉ giới hạn ở các thư viện, Module Augmentation còn được dùng để định nghĩa lại các biến toàn cục (Global Variables) của chính Node.js, điển hình nhất là process.env.

Mặc định, process.env được TypeScript hiểu là kiểu Record<string, string | undefined> (một object chứa các string ngẫu nhiên). Điều này khiến việc gọi biến môi trường rất dễ sai chính tả.

Chúng ta có thể hack thẳng vào object ProcessEnv của NodeJS:

// types/node/index.d.ts

declare namespace NodeJS {
  interface ProcessEnv {
    PORT: string;
    DATABASE_URL: string;
    JWT_SECRET_KEY: string;
    NODE_ENV: 'development' | 'production' | 'test'; // Thậm chí có thể siết chặt bằng Union Types
  }
}

Từ giây phút này, bất cứ nơi nào bạn gõ process.env., IDE sẽ tự động xổ ra danh sách PORT, DATABASE_URL, v.v. Nếu bạn gõ sai tên biến môi trường, code sẽ lập tức báo lỗi đỏ, cứu bạn khỏi những cú "sập server" ngớ ngẩn khi deploy lên Production.

Bằng việc thấu hiểu cơ chế Declaration Merging và Module Augmentation, bạn không còn là "nạn nhân" của các thư viện bên thứ 3 nữa. Bạn hoàn toàn có quyền làm chủ và nhào nặn lại mọi Interface trong hệ sinh thái Node.js để chúng phục vụ chính xác cho nhu cầu nghiệp vụ của dự án!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.