Plik deklaracji (.d.ts) zawiera typy i nic więcej: sygnatury, interfejsy i kształty klas dla JavaScriptu, który znajduje się gdzie indziej. Słowo kluczowe declare wykonuje to samo zadanie w zwykłym pliku. Mówi kompilatorowi „to istnieje w czasie działania, zaufaj mi” i nie emituje żadnego kodu.
Kompilator zaakceptował __APP_VERSION__.length, bo deklaracja mówi, że to napis. W czasie działania taka zmienna nie istnieje, więc odczyt .length rzuca ReferenceError: __APP_VERSION__ is not defined, czyli wyjątek, którego kompilator nie mógł przewidzieć. Tak wygląda umowa każdej deklaracji: typy są prawdziwe tylko w takim stopniu, w jakim prawdziwy jest stojący za nimi JavaScript.
Co trafia do pliku .d.ts
Najlepiej pokazuje to kompilacja z declaration: true. Z takiego kodu źródłowego:
// 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 zapisuje price.js i taki 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;
}
Ciała funkcji zniknęły, wywnioskowane typy zwracane zostały wypisane (number, this), pole prywatne zachowało nazwę, ale straciło typ, a nieeksportowanego TAX w ogóle nie ma. Biblioteka publikuje .js dla Node i .d.ts dla twojego edytora i kompilatora. emitDeclarationOnly: true tworzy tylko pliki .d.ts, dla projektów, w których JavaScript buduje bundler.
Wbudowane elementy ES2022, których używasz na co dzień, też pochodzą z plików deklaracji: lib.es2022.d.ts i pokrewne pliki są dostarczane z TypeScriptem i wybierane przez target i lib.
Formy declare
Każda instrukcja declare opisuje coś, co już istnieje. W pliku .d.ts bez import i export (globalnym pliku deklaracji) każda z nich staje się widoczna w całym projekcie:
// 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;
}
Wewnątrz pliku .d.ts interface i type nie potrzebują declare; każda inna deklaracja najwyższego poziomu bez declare albo export to błąd TS1046. Gdy plik ma import albo export, jego deklaracje są lokalne, a dodatki do zakresu globalnego trafiają do bloku declare global (pokazanego niżej). Zadeklarowana sygnatura to to, względem czego kompilator sprawdza wywołania:
Kompilator zgłasza index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Przekaż { items: "3" } albo zmień deklarację, jeśli prawdziwa funkcja przyjmuje liczby.
Pakiety @types
Wiele pakietów npm dostarcza własne pliki .d.ts, wskazane w polu types (albo w warunku types w exports) swojego package.json. Dla pakietów napisanych tylko w JavaScripcie projekt DefinitelyTyped, utrzymywany przez społeczność, publikuje typy w zakresie @types:
npm i lodash
npm i -D @types/lodash
Gdy importujesz (import) pakiet, TypeScript automatycznie szuka node_modules/@types/{name}. Typy, które opisują zmienne globalne, a nie import, takie jak @types/node (process, Buffer) albo describe i it z narzędzia do testów, muszą być wymienione w tsconfig.json:
{
"compilerOptions": {
"types": ["node"]
}
}
Bez tego wpisu TypeScript 7 ich nie wczytuje, nawet gdy pakiet jest zainstalowany:
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 zapisuje w nowej konfiguracji "types": [] z komentarzem, który dla projektów Node sugeruje ["node"].
Typowanie modułu bez typów
Import pakietu JavaScript, który nie ma typów ani w paczce, ani w @types, to błąd TS7016:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
Napraw to plikiem .d.ts w dowolnym miejscu projektu (popularny jest folder types/; musi tylko być objęty przez include), który nie ma import ani export na najwyższym poziomie. Opisz części, których używasz:
// 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;
}
Najkrótsza wersja, declare module "fakelib"; w jednej linii, sprawia, że każdy import z pakietu jest any. Usuwa błąd, a razem z nim wszystkie sprawdzenia, więc traktuj ją jako krok tymczasowy.
declare global
Kod, który dodaje coś do zakresu globalnego, na przykład polyfill, nowa metoda wbudowanego typu albo zmienna globalna ustawiana przez znacznik script, wymaga pasujących typów. Dodaje je declare global. Musi być w module (pliku z import lub export), dlatego na początku jest export {}:
interface Array<T> łączy się z wbudowanym interfejsem Array zamiast go zastępować, a to właśnie var (nie let ani const) dodaje właściwość do globalThis. Rozszerzanie wbudowanych prototypów jest ryzykowne we współdzielonym kodzie; tą samą techniką declare global projekty dodają własne zmienne do process.env w interfejsie NodeJS.ProcessEnv z @types/node.
Rozszerzanie modułów
Żeby dodać coś do typów importowanego pakietu, zadeklaruj ponownie nazwę jego modułu i otwórz ponownie interfejs. Sam plik musi być modułem (załatwia to import; export {} też działa). Bez import ani export ten sam blok deklaruje zupełnie nowy moduł config-lib, który przesłania prawdziwe typy pakietu:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
Od tej pory load().beta z config-lib ma wszędzie typ boolean. Interfejsy się łączą, aliasy typów nie, więc żeby to zadziałało, biblioteka musi eksportować interface. W ten sposób wtyczki dodają pola do obiektów żądania albo konfiguracji frameworka.
skipLibCheck
skipLibCheck: true sprawia, że kompilator nie sprawdza typów w plikach .d.ts, także tych w node_modules. Twój własny kod nadal jest względem nich sprawdzany. Oszczędza to czas i pozwala uniknąć błędów z dwóch pakietów, których deklaracje sobie przeczą, dlatego tsc --init włącza tę opcję. Kosztem jest to, że błąd we własnych plikach .d.ts też nie zostanie zgłoszony.
Najczęściej zadawane pytania
Czym jest plik .d.ts w TypeScript?
To plik deklaracji: zawiera tylko typy (sygnatury funkcji, interfejsy, kształty klas), bez implementacji. Opisuje JavaScript, który istnieje gdzie indziej, na przykład skompilowaną bibliotekę albo wbudowane API przeglądarki, żeby TypeScript mógł sprawdzać kod, który z niego korzysta. tsc nigdy nie emituje dla niego JavaScriptu.
Co robi słowo kluczowe declare w TypeScript?
Mówi kompilatorowi, że wartość istnieje w czasie działania, ale jej nie tworzy. declare const VERSION: string; pozwala używać VERSION jako napisu, a sama linia znika z wyniku. Jeśli nic naprawdę nie definiuje VERSION, program kończy się w czasie działania błędem ReferenceError.
Jak naprawić "Could not find a declaration file for module"?
Błąd TS7016 oznacza, że pakiet nie ma typów. Zainstaluj je, jeśli istnieją (npm i -D @types/package-name), albo dodaj plik .d.ts z declare module "package-name" { ... } opisującym to, czego używasz. Samo declare module "package-name"; wycisza błąd, typując cały moduł jako any.
Jak wygenerować pliki .d.ts z TypeScript?
Ustaw "declaration": true w tsconfig.json (albo przekaż --declaration). Każdy plik .ts tworzy wtedy .d.ts obok swojego .js. Dodaj emitDeclarationOnly, gdy JavaScript buduje inne narzędzie, oraz declarationMap, żeby edytory mogły przechodzić od typów do twojego kodu źródłowego.
Czym różni się declare global od declare module?
declare global { ... } dodaje coś do zakresu globalnego, na przykład nową właściwość Array albo zmienną globalną. declare module "name" { ... } opisuje moduł importowany pod tą nazwą. declare global musi być w module (pliku z import lub export); declare module deklaruje nowy moduł w pliku bez importów i eksportów, a wewnątrz modułu rozszerza istniejący.