קובץ הצהרה (.d.ts) מכיל טיפוסים ותו לא: חתימות, interfaces ומבנים של מחלקות עבור JavaScript שנמצא במקום אחר. מילת המפתח declare עושה את אותה עבודה בתוך קובץ רגיל. היא אומרת לקומפיילר "זה קיים בזמן ריצה, סמוך עליי", ולא מייצרת שום קוד.
הקומפיילר קיבל את __APP_VERSION__.length כי ההצהרה אומרת שזו מחרוזת. בזמן ריצה אין משתנה כזה, ולכן קריאת .length זורקת ReferenceError: __APP_VERSION__ is not defined, חריגה בזמן ריצה שהקומפיילר לא ראה מגיעה. זה החוזה של כל הצהרה: הטיפוסים נכונים רק במידה שה-JavaScript שמאחוריהם נכון.
מה נכנס לקובץ .d.ts
קומפילציה עם declaration: true מדגימה את הרעיון הכי טוב. מהקוד הזה:
// 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 כותב את price.js ואת ה-price.d.ts הזה:
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;
}
גופי הפונקציות נעלמו, טיפוסי ההחזרה שהוסקו נכתבים במפורש (number, this), השדה הפרטי שומר על השם שלו אבל מאבד את הטיפוס, ו-TAX שלא יוצא לא מופיע בכלל. ספרייה מפרסמת את ה-.js בשביל Node ואת ה-.d.ts בשביל העורך והקומפיילר שלכם. emitDeclarationOnly: true מייצר רק את קבצי ה-.d.ts, לפרויקטים שבהם bundler בונה את ה-JavaScript.
גם הרכיבים המובנים של ES2022 שאתם משתמשים בהם כל יום מגיעים מקבצי הצהרה: lib.es2022.d.ts והחברים שלו מגיעים עם TypeScript ונבחרים לפי target ו-lib.
הצורות של declare
כל פקודת declare מתארת משהו שכבר קיים. בקובץ .d.ts בלי import או export (קובץ הצהרה גלובלי), כל אחת מאלה נהיית גלויה לכל הפרויקט:
// 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;
}
בתוך קובץ .d.ts, interface ו-type לא צריכים declare; כל הצהרה אחרת ברמה העליונה בלי declare או export היא השגיאה TS1046. ברגע שיש לקובץ import או export, ההצהרות שלו מקומיות לו, ותוספות ל-scope הגלובלי נכנסות לבלוק declare global (מוצג בהמשך). החתימה המוצהרת היא מה שהקומפיילר בודק מולו קריאות:
הקומפיילר מדווח index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. העבירו { items: "3" }, או שנו את ההצהרה אם הפונקציה האמיתית מקבלת מספרים.
חבילות @types
הרבה חבילות npm מגיעות עם קבצי .d.ts משלהן, שמופנים מהשדה types (או מתנאי types ב-exports) ב-package.json שלהן. לחבילות של JavaScript בלבד, הפרויקט DefinitelyTyped, שמתוחזק על ידי הקהילה, מפרסם טיפוסים תחת ה-scope של @types:
npm i lodash
npm i -D @types/lodash
כשאתם עושים import לחבילה, TypeScript מחפשת את node_modules/@types/{name} אוטומטית. טיפוסים שמתארים ערכים גלובליים ולא import, כמו @types/node (process, Buffer) או ה-describe וה-it של test runner, חייבים להופיע ב-tsconfig.json:
{
"compilerOptions": {
"types": ["node"]
}
}
בלי הרשומה הזו, TypeScript 7 לא טוענת אותם, גם כשהחבילה מותקנת:
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 כותב "types": [] לקונפיגורציה החדשה, עם הערה שמציעה ["node"] לפרויקטי Node.
טיפוסים למודול בלי טיפוסים
ייבוא של חבילת JavaScript שאין לה טיפוסים, לא מצורפים ולא ב-@types, הוא השגיאה TS7016:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
תקנו את זה עם קובץ .d.ts בכל מקום בפרויקט (תיקיית types/ נפוצה; היא רק צריכה להיות מכוסה על ידי include) שאין בו import או export ברמה העליונה. תארו את החלקים שאתם משתמשים בהם:
// 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;
}
הגרסה הקצרה ביותר, declare module "fakelib"; בשורה אחת, הופכת כל ייבוא מהחבילה ל-any. היא מסירה את השגיאה ואיתה כל בדיקה, אז התייחסו אליה כצעד זמני.
declare global
קוד שמוסיף ל-scope הגלובלי, כמו polyfill, מתודה חדשה על רכיב מובנה או ערך גלובלי שנקבע על ידי תגית script, צריך את הטיפוסים המתאימים. declare global מוסיף אותם. הוא חייב לשבת בתוך מודול (קובץ עם import או export), ובגלל זה export {} נמצא בראש הקובץ:
interface Array<T> מתמזג עם ה-interface המובנה Array במקום להחליף אותו, ו-var (לא let או const) הוא מה שמוסיף מאפיין ל-globalThis. הרחבה של prototypes מובנים מסוכנת בקוד משותף; אותה טכניקה של declare global היא הדרך שבה פרויקטים מוסיפים משתנים משלהם ל-process.env ב-interface NodeJS.ProcessEnv של @types/node.
Module Augmentation
כדי להוסיף לטיפוסים של חבילה שאתם מייבאים, הצהירו מחדש על שם המודול שלה ופתחו מחדש את ה-interface. הקובץ חייב להיות מודול בעצמו (ה-import דואג לזה; גם export {} עובד). בלי import או export, אותו בלוק מצהיר על מודול config-lib חדש לגמרי שמסתיר את הטיפוסים האמיתיים של החבילה:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
אחרי זה, load().beta מ-config-lib מקבל את הטיפוס boolean בכל מקום. interfaces מתמזגים; type aliases לא, ולכן ספרייה צריכה לייצא interface כדי שזה יעבוד. כך plugins מוסיפים שדות לאובייקטי request או config של framework.
skipLibCheck
skipLibCheck: true עוצר את הקומפיילר מלבדוק טיפוסים בקבצי .d.ts, כולל אלה שב-node_modules. הקוד שלכם עדיין נבדק מולם. זה חוסך זמן ומונע שגיאות משתי חבילות שההצהרות שלהן לא מסכימות, ובגלל זה tsc --init מפעיל אותו. המחיר הוא שגם טעות בתוך קבצי ה-.d.ts שלכם לא מדווחת.
שאלות נפוצות
מה זה קובץ .d.ts ב-TypeScript?
קובץ הצהרה: הוא מכיל רק טיפוסים (חתימות של פונקציות, interfaces, מבנים של מחלקות) ובלי מימוש. הוא מתאר JavaScript שקיים במקום אחר, כמו ספרייה מקומפלת או ה-APIs המובנים של הדפדפן, כדי ש-TypeScript תוכל לבדוק קוד שמשתמש בו. tsc אף פעם לא מייצר עבורו JavaScript.
מה עושה מילת המפתח declare ב-TypeScript?
היא אומרת לקומפיילר שערך קיים בזמן ריצה, בלי ליצור אותו. declare const VERSION: string; מאפשר להשתמש ב-VERSION כמחרוזת, והשורה נעלמת מהפלט. אם שום דבר לא מגדיר בפועל את VERSION, התוכנית נכשלת בזמן ריצה עם ReferenceError.
איך מתקנים את "Could not find a declaration file for module"?
השגיאה TS7016 אומרת שלחבילה אין טיפוסים. התקינו אותם אם הם קיימים (npm i -D @types/package-name), או הוסיפו קובץ .d.ts עם declare module "package-name" { ... } שמתאר את מה שאתם משתמשים בו. declare module "package-name"; לבדו משתיק את השגיאה כי הוא נותן לכל המודול את הטיפוס any.
איך מייצרים קבצי .d.ts מ-TypeScript?
הגדירו "declaration": true ב-tsconfig.json (או העבירו --declaration). אז כל קובץ .ts מייצר .d.ts ליד ה-.js שלו. הוסיפו emitDeclarationOnly כשכלי אחר בונה את ה-JavaScript, ו-declarationMap כדי שעורכים יוכלו לקפוץ מהטיפוסים לקוד המקור שלכם.
מה ההבדל בין declare global ל-declare module?
declare global { ... } מוסיף ל-scope הגלובלי, למשל מאפיין חדש על Array או משתנה גלובלי. declare module "name" { ... } מתאר מודול שמייבאים בשם הזה. declare global חייב לשבת בתוך מודול (קובץ עם import או export); declare module מצהיר על מודול חדש בקובץ בלי imports או exports, ומרחיב מודול קיים כשהוא בתוך מודול.