Tệp khai báo (declaration file, .d.ts) chỉ chứa kiểu và không gì khác: chữ ký hàm, interface và hình dạng class cho JavaScript nằm ở nơi khác. Từ khóa declare làm cùng việc đó bên trong một tệp bình thường. Nó báo cho trình biên dịch rằng "thứ này tồn tại lúc chạy, cứ tin đi", và không sinh ra code nào.
Trình biên dịch chấp nhận __APP_VERSION__.length vì lời khai báo nói đó là một chuỗi. Lúc chạy không có biến nào như vậy, nên đọc .length sẽ ném ra ReferenceError: __APP_VERSION__ is not defined, một ngoại lệ lúc chạy mà trình biên dịch không hề lường trước. Đó là giao kèo của mọi lời khai báo: kiểu chỉ đúng khi JavaScript phía sau nó đúng.
Tệp .d.ts chứa những gì
Biên dịch với declaration: true cho thấy rõ ý tưởng này nhất. Từ mã nguồn này:
// price.ts
export interface LineItem {
name: string;
price: number;
qty: number;
}
const TAX = 0.2;
export function total(items: LineItem[]) {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
return Math.round(sum * (1 + TAX) * 100) / 100;
}
export class Cart {
private items: LineItem[] = [];
add(item: LineItem) {
this.items.push(item);
return this;
}
}
tsc viết ra price.js và tệp price.d.ts này:
export interface LineItem {
name: string;
price: number;
qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
private items;
add(item: LineItem): this;
}
Thân hàm biến mất, kiểu trả về được suy ra thì được viết rõ (number, this), thuộc tính private giữ tên nhưng mất kiểu, còn TAX không được export thì hoàn toàn không có mặt. Một thư viện phát hành tệp .js cho Node và tệp .d.ts cho trình soạn thảo và trình biên dịch của bạn. emitDeclarationOnly: true chỉ tạo ra các tệp .d.ts, dành cho dự án dùng bundler để build JavaScript.
Các đối tượng có sẵn của ES2022 mà bạn dùng hằng ngày cũng đến từ tệp khai báo: lib.es2022.d.ts và các tệp cùng loại đi kèm TypeScript và được chọn qua target và lib.
Các dạng declare
Mỗi câu lệnh declare mô tả một thứ đã tồn tại. Trong một tệp .d.ts không có import hay export (một tệp khai báo toàn cục), mỗi khai báo sau đây đều hiện diện trong toàn bộ dự án:
// globals.d.ts
declare const API_URL: string; // a global constant
declare let debugMode: boolean; // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget { // a class from a script tag
constructor(el: string);
render(): void;
}
declare namespace Analytics { // a global object
function page(name: string): void;
}
declare module "legacy-charts" { // a module you import
export function draw(data: number[]): void;
}
Bên trong tệp .d.ts, interface và type không cần declare; mọi khai báo cấp cao nhất khác mà không có declare hay export là lỗi TS1046. Khi một tệp có import hoặc export, các khai báo của nó chỉ có tác dụng cục bộ trong tệp, và những phần bổ sung vào phạm vi toàn cục phải nằm trong khối declare global (xem bên dưới). Chữ ký được khai báo là thứ trình biên dịch dùng để kiểm tra lời gọi:
Trình biên dịch báo index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Hãy truyền { items: "3" }, hoặc sửa lời khai báo nếu hàm thật nhận số.
Package @types
Nhiều package npm đi kèm sẵn tệp .d.ts, được tham chiếu từ trường types (hoặc một điều kiện types trong exports) của package.json. Với các package chỉ có JavaScript, dự án DefinitelyTyped do cộng đồng duy trì phát hành kiểu dưới scope @types:
npm i lodash
npm i -D @types/lodash
Khi bạn import một package, TypeScript tự động tìm node_modules/@types/{name}. Những kiểu mô tả biến toàn cục thay vì một thứ được import, như @types/node (process, Buffer) hay describe và it của một test runner, phải được liệt kê trong tsconfig.json:
{
"compilerOptions": {
"types": ["node"]
}
}
Thiếu mục đó, TypeScript 7 không nạp chúng, kể cả khi package đã được cài:
error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
tsc --init ghi "types": [] vào cấu hình mới, kèm một chú thích gợi ý dùng ["node"] cho dự án Node.
Gán kiểu cho module chưa có kiểu
Import một package JavaScript không có kiểu, dù đi kèm hay trong @types, là lỗi TS7016:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
Hãy sửa bằng một tệp .d.ts ở bất kỳ đâu trong dự án (thư mục types/ là lựa chọn phổ biến; chỉ cần nó nằm trong phạm vi include) và không có import hay export ở cấp cao nhất. Mô tả những phần bạn dùng:
// types/fakelib.d.ts
declare module "fakelib" {
export function hi(name: string): string;
export const version: string;
}
// Non-code files a bundler lets you import
declare module "*.svg" {
const url: string;
export default url;
}
Phiên bản ngắn nhất, declare module "fakelib"; trên một dòng, làm mọi thứ import từ package trở thành any. Nó xóa lỗi nhưng cũng xóa luôn mọi phép kiểm tra, nên chỉ coi đó là bước tạm thời.
declare global
Code bổ sung vào phạm vi toàn cục, như một polyfill, một phương thức mới trên đối tượng có sẵn, hay một biến toàn cục do thẻ script đặt ra, cần có kiểu tương ứng. declare global thêm các kiểu đó. Nó phải nằm trong một module (một tệp có import hoặc export), đó là lý do export {} nằm ở đầu tệp:
interface Array<T> hợp nhất với interface Array có sẵn thay vì thay thế nó, và var (không phải let hay const) mới là thứ thêm thuộc tính vào globalThis. Mở rộng prototype của đối tượng có sẵn là rủi ro trong code dùng chung; cũng kỹ thuật declare global này là cách các dự án thêm biến riêng vào process.env qua interface NodeJS.ProcessEnv của @types/node.
Module augmentation
Để bổ sung vào kiểu của một package bạn import, hãy khai báo lại tên module của nó và mở lại interface. Bản thân tệp phải là một module (import làm điều đó; export {} cũng được). Không có import hay export, cùng khối code đó sẽ khai báo một module config-lib hoàn toàn mới, che mất kiểu thật của package:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
Sau đó, load().beta từ config-lib có kiểu boolean ở mọi nơi. Interface hợp nhất được; type alias thì không, nên thư viện phải export một interface thì cách này mới dùng được. Đây là cách các plugin thêm trường vào đối tượng request hoặc config của một framework.
skipLibCheck
skipLibCheck: true khiến trình biên dịch không kiểm tra kiểu các tệp .d.ts, kể cả các tệp trong node_modules. Code của bạn vẫn được kiểm tra dựa trên chúng. Tùy chọn này tiết kiệm thời gian và tránh lỗi từ hai package có khai báo mâu thuẫn nhau, đó là lý do tsc --init bật nó. Cái giá là lỗi bên trong chính các tệp .d.ts của bạn cũng không được báo.
Câu hỏi thường gặp
Tệp .d.ts trong TypeScript là gì?
Là một tệp khai báo: nó chỉ chứa kiểu (chữ ký hàm, interface, hình dạng class) và không có phần hiện thực. Nó mô tả JavaScript nằm ở nơi khác, chẳng hạn một thư viện đã biên dịch hay các API có sẵn của trình duyệt, để TypeScript kiểm tra được code dùng đến chúng. tsc không bao giờ sinh JavaScript cho tệp này.
Từ khóa declare trong TypeScript dùng để làm gì?
Nó báo cho trình biên dịch biết một giá trị tồn tại lúc chạy mà không tạo ra giá trị đó. declare const VERSION: string; cho phép bạn dùng VERSION như một chuỗi, và dòng này biến mất khỏi kết quả biên dịch. Nếu không có gì thật sự định nghĩa VERSION, chương trình sẽ lỗi lúc chạy với ReferenceError.
Sửa lỗi "Could not find a declaration file for module" thế nào?
Lỗi TS7016 nghĩa là package không có kiểu. Hãy cài kiểu nếu có (npm i -D @types/package-name), hoặc thêm một tệp .d.ts với declare module "package-name" { ... } mô tả những gì bạn dùng. Chỉ riêng declare module "package-name"; cũng dập được lỗi bằng cách gán kiểu any cho cả module.
Làm sao sinh tệp .d.ts từ TypeScript?
Đặt "declaration": true trong tsconfig.json (hoặc truyền --declaration). Khi đó mỗi tệp .ts tạo ra một tệp .d.ts cạnh tệp .js của nó. Thêm emitDeclarationOnly khi một công cụ khác build JavaScript, và declarationMap để trình soạn thảo nhảy được từ kiểu về mã nguồn của bạn.
declare global và declare module khác nhau thế nào?
declare global { ... } bổ sung vào phạm vi toàn cục, ví dụ một thuộc tính mới trên Array hoặc một biến toàn cục. declare module "name" { ... } mô tả một module mà bạn import bằng tên đó. declare global phải nằm trong một module (một tệp có import hoặc export); declare module khai báo một module mới trong tệp không có import hay export, và bổ sung (augment) cho một module có sẵn khi nằm trong một module.