Declaration Files (.d.ts)

Bài 10: Declaration Files (.d.ts)

Bạn cài một thư viện JavaScript thuần, import vào dự án TypeScript, và thấy: Could not find a declaration file for module 'some-lib'. Phản xạ đầu tiên là npm i @types/some-lib — nhưng nếu package không tồn tại trên DefinitelyTyped thì sao?

Đây là lúc bạn cần biết cách viết .d.ts files.


#Tại sao .d.ts tồn tại?

TypeScript cần type information để kiểm tra code. Khi bạn import một thư viện JS thuần, TypeScript không có cách nào biết API của nó trông ra sao.

src/
  index.ts          ← TypeScript code của bạn
node_modules/
  some-lib/
    index.js        ← JavaScript thuần, không có type
    package.json    ← Không có "types" field

Giải pháp: .d.ts files — chúng chỉ chứa type declarations, không sinh ra code JavaScript nào.


#Vấn đề: Dùng any để bypass

Anti-pattern phổ biến nhất:

// ❌ Anti-pattern: dùng any để tránh lỗi
const someLib = require("some-lib") as any;
someLib.anything.goes.here; // Không type checking — vô nghĩa
// ❌ Anti-pattern: dùng // @ts-ignore
// @ts-ignore
const result = someLib.doSomething(); // Bỏ qua lỗi — giấu bug

Cả hai cách đều biến TypeScript thành JavaScript. Hãy viết declaration đúng cách.


#declare module — Cách cơ bản nhất

Tạo file .d.ts để khai báo type cho thư viện không có type.

Ví dụ: Thư viện greeting chỉ export một function:

// ❌ Không có declaration — TypeScript không biết type
import { greet } from "greeting";
greet(123, true, null); // Không có type checking
// ✅ declarations/greeting.d.ts
declare module "greeting" {
  export function greet(name: string, formal?: boolean): string;
  export const version: string;
}
// ✅ Bây giờ có type checking đầy đủ
import { greet, version } from "greeting";
 
greet("An"); // ✅ OK
greet("An", true); // ✅ OK
greet(123); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'

Khai báo file .d.ts trong tsconfig:

{
  "compilerOptions": {
    "typeRoots": ["./declarations", "./node_modules/@types"],
    "types": ["greeting"]
  }
}

Hoặc đơn giản hơn — chỉ cần file .d.ts nằm trong include paths:

{
  "include": ["src", "declarations"]
}

#declare global — Mở rộng global scope

Khi bạn cần thêm type cho window, global, hoặc các biến global khác.

// ✅ global.d.ts
declare global {
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string;
      version: string;
    };
  }
 
  // Thêm global function
  function __DEV__(): boolean;
}
 
export {}; // File phải có export để TypeScript hiểu đây là module
// ✅ Bây giờ truy cập được với type safety
const config = window.__APP_CONFIG__;
console.log(config.apiUrl); // ✅ TypeScript biết apiUrl là string
 
if (__DEV__()) {
  console.log("Development mode");
}

Lưu ý: declare global phải nằm trong file .d.ts hoặc trong file .ts có ít nhất một export statement.


#Ambient Types — declare và namespace

#declare var / let / const

Khai báo biến tồn tại ở runtime nhưng không có trong source TypeScript.

// ✅ env.d.ts
declare const __API_URL__: string;
declare const __BUILD_TIME__: string;
declare let __ENABLE_DEBUG__: boolean;
// Dùng trực tiếp — Vite/esbuild sẽ inject giá trị thật
fetch(__API_URL__ + "/users");

#declare function

// ✅ Declare function global
declare function setTimeout(callback: () => void, ms: number): number;

#declare class

// ✅ Declare class từ native binding
declare class SQLiteDatabase {
  constructor(path: string);
  prepare(sql: string): Statement;
  close(): void;
}

#Namespace — nhóm type liên quan

// ✅ namespace.d.ts
declare namespace Api {
  interface User {
    id: string;
    name: string;
    email: string;
  }
 
  interface Post {
    id: string;
    title: string;
    authorId: string;
  }
 
  type Response<T> = {
    data: T;
    error: string | null;
  };
}
// ✅ Sử dụng
const user: Api.User = { id: "1", name: "An", email: "an@example.com" };
const response: Api.Response<Api.User> = {
  data: user,
  error: null,
};

#@types Packages — DefinitelyTyped

Thư viện phổ biến có sẵn type trên DefinitelyTyped.

npm install lodash
npm install -D @types/lodash

Cách hoạt động: @types/lodash chứa file .d.ts khai báo type cho mọi function trong lodash.

import _ from "lodash";
 
_.chunk([1, 2, 3, 4], 2); // ✅ TypeScript biết trả về number[][]
_.camelCase("Hello World"); // ✅ TypeScript biết trả về string
_.sumBy([{ n: 1 }, { n: 2 }], "n"); // ✅ Type-safe

Lưu ý: Không phải thư viện nào cũng có @types. Khi đó bạn phải tự viết .d.ts.


#Module Augmentation — Mở rộng type của thư viện khác

Khi bạn muốn thêm method vào prototype hoặc mở rộng interface của thư viện.

// ✅ Mở rộng Express Request
// types/express.d.ts
declare module "express-serve-static-core" {
  interface Request {
    user?: {
      id: string;
      role: "admin" | "user";
    };
    sessionId?: string;
  }
}
// ✅ Bây giờ truy cập req.user mà không cần casting
import { Request, Response } from "express";
 
function authMiddleware(req: Request, res: Response) {
  if (!req.user) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  console.log(req.user.role); // ✅ TypeScript biết role là "admin" | "user"
}

#Conditional Types trong .d.ts

Khi type phụ thuộc vào điều kiện:

// ✅ api.d.ts
declare module "api-client" {
  interface ApiClient {
    get<T>(url: string): Promise<T>;
    post<T>(url: string, body: unknown): Promise<T>;
 
    // Overload — type khác nhau tùy method
    request(config: { method: "GET"; url: string }): Promise<unknown>;
    request(config: { method: "POST"; url: string; body: unknown }): Promise<unknown>;
  }
 
  const client: ApiClient;
  export default client;
}

#Best Practices cho .d.ts files

1. Tổ chức file theo scope:

types/
  global.d.ts          ← declare global
  modules.d.ts         ← declare module cho thư viện
  env.d.ts             ← declare cho environment variables

2. Luôn export rỗng trong global .d.ts:

declare global {
  // ...
}
export {}; // Bắt buộc nếu file không có export nào khác

3. Sử dụng /// <reference types="..." /> khi cần:

/// <reference types="node" />
/// <reference types="jest" />

4. Không lạm dụng declare module wildcards:

// ❌ Quá rộng — mọi import sẽ match
declare module "*";
 
// ✅ Module cụ thể
declare module "specific-lib";

#Bài tập

#Dễ

Viết .d.ts cho thư viện math-utils export 3 function: add(a: number, b: number), multiply(a: number, b: number), PI (hằng số).

#Trung bình

Viết .d.ts cho thư viện logger có interface Logger với methods info, warn, error — mỗi method nhận message: string và optional context: Record<string, unknown>.

#Khó

Viết module augmentation mở rộng Express.Request thêm currentUser với type { id: string; email: string; permissions: string[] } và viết middleware sử dụng nó.


#Tổng kết

  • .d.ts files chỉ chứa type declarations, không tạo runtime code
  • declare module khai báo type cho thư viện không có TypeScript
  • declare global mở rộng global scope (window, globalThis)
  • @types packages từ DefinitelyTyped cung cấp type cho thư viện JS phổ biến
  • Module augmentation cho phép mở rộng type của thư viện khác
  • Tuyệt đối không dùng any chỉ vì thiếu type — hãy viết .d.ts

Bài tiếp theo: Function Overloads & Generic Inference — cách viết function có nhiều signature và kiểm soát inference.