Eine Deklarationsdatei (.d.ts) enthält Typen und sonst nichts: Signaturen, Interfaces und Klassenformen für JavaScript, das woanders liegt. Das Schlüsselwort declare erfüllt dieselbe Aufgabe in einer gewöhnlichen Datei. Es sagt dem Compiler „das existiert zur Laufzeit, vertrau mir“ und erzeugt keinen Code.
Der Compiler hat __APP_VERSION__.length akzeptiert, weil die Deklaration sagt, es sei ein String. Zur Laufzeit existiert keine solche Variable, also wirft das Lesen von .length den Fehler ReferenceError: __APP_VERSION__ is not defined, eine Ausnahme zur Laufzeit, die der Compiler nicht kommen sah. Das ist der Vertrag jeder Deklaration: Die Typen stimmen nur so weit wie das JavaScript dahinter.
Was in einer .d.ts-Datei steht
Am besten zeigt das Kompilieren mit declaration: true die Idee. Aus diesem Quellcode:
// 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;
}
}
schreibt tsc die Datei price.js und diese 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;
}
Die Funktionsrümpfe sind weg, abgeleitete Rückgabetypen werden ausgeschrieben (number, this), das private Feld behält seinen Namen, verliert aber seinen Typ, und das nicht exportierte TAX fehlt ganz. Eine Bibliothek veröffentlicht die .js für Node und die .d.ts für deinen Editor und Compiler. emitDeclarationOnly: true erzeugt nur die .d.ts-Dateien, für Projekte, in denen ein Bundler das JavaScript baut.
Auch die ES2022-Built-ins, die du täglich nutzt, kommen aus Deklarationsdateien: lib.es2022.d.ts und verwandte Dateien werden mit TypeScript ausgeliefert und über target und lib ausgewählt.
Die declare-Formen
Jede declare-Anweisung beschreibt etwas, das bereits existiert. In einer .d.ts-Datei ohne import oder export (einer globalen Deklarationsdatei) wird jede davon im ganzen Projekt sichtbar:
// 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;
}
In einer .d.ts-Datei brauchen interface und type kein declare; jede andere Deklaration auf oberster Ebene ohne declare oder export ist der Fehler TS1046. Sobald eine Datei ein import oder export hat, sind ihre Deklarationen lokal, und Ergänzungen des globalen Scopes gehören in einen Block declare global (unten gezeigt). Gegen die deklarierte Signatur prüft der Compiler Aufrufe:
Der Compiler meldet index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Übergib { items: "3" } oder ändere die Deklaration, falls die echte Funktion Zahlen akzeptiert.
@types-Pakete
Viele npm-Pakete liefern eigene .d.ts-Dateien mit, auf die das Feld types (oder eine Bedingung types in exports) ihrer package.json verweist. Für reine JavaScript-Pakete veröffentlicht das von der Community gepflegte Projekt DefinitelyTyped Typen unter dem Scope @types:
npm i lodash
npm i -D @types/lodash
Wenn du ein Paket mit import einbindest, sucht TypeScript automatisch nach node_modules/@types/{name}. Typen, die Globals statt eines Imports beschreiben, etwa @types/node (process, Buffer) oder describe und it eines Test-Runners, müssen in tsconfig.json aufgeführt werden:
{
"compilerOptions": {
"types": ["node"]
}
}
Ohne diesen Eintrag lädt TypeScript 7 sie nicht, selbst wenn das Paket installiert ist:
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 schreibt "types": [] in die neue Konfiguration, mit einem Kommentar, der für Node-Projekte ["node"] vorschlägt.
Ein untypisiertes Modul typisieren
Ein JavaScript-Paket ohne Typen zu importieren, weder mitgeliefert noch in @types, ist der Fehler TS7016:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
Behebe das mit einer .d.ts-Datei irgendwo in deinem Projekt (ein Ordner types/ ist üblich; er muss nur von include abgedeckt sein), die kein import oder export auf oberster Ebene hat. Beschreibe die Teile, die du nutzt:
// 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;
}
Die kürzeste Variante, declare module "fakelib"; in einer Zeile, macht jeden Import aus dem Paket zu any. Sie beseitigt den Fehler und mit ihm jede Prüfung, betrachte sie also als vorübergehenden Schritt.
declare global
Code, der den globalen Scope ergänzt, etwa ein Polyfill, eine neue Methode auf einem Built-in oder eine globale Variable aus einem Script-Tag, braucht die passenden Typen. declare global fügt sie hinzu. Es muss in einem Modul stehen (einer Datei mit import oder export), deshalb steht export {} ganz oben:
interface Array<T> verschmilzt mit dem eingebauten Interface Array, statt es zu ersetzen, und var (nicht let oder const) ist das, was eine Eigenschaft zu globalThis hinzufügt. Eingebaute Prototypen zu erweitern ist in gemeinsam genutztem Code riskant; mit derselben declare global-Technik fügen Projekte ihre eigenen Variablen zu process.env im Interface NodeJS.ProcessEnv von @types/node hinzu.
Module Augmentation
Um die Typen eines importierten Pakets zu ergänzen, deklarierst du seinen Modulnamen erneut und öffnest das Interface wieder. Die Datei muss selbst ein Modul sein (das import sorgt dafür; export {} geht auch). Ohne import oder export deklariert derselbe Block ein brandneues Modul config-lib, das die echten Typen des Pakets verdeckt:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
Danach ist load().beta aus config-lib überall als boolean typisiert. Interfaces verschmelzen, Typaliasse nicht, eine Bibliothek muss also ein interface exportieren, damit das funktioniert. So fügen Plugins den Request- oder Config-Objekten eines Frameworks Felder hinzu.
skipLibCheck
skipLibCheck: true hält den Compiler davon ab, .d.ts-Dateien zu prüfen, auch die in node_modules. Dein eigener Code wird weiterhin dagegen geprüft. Das spart Zeit und vermeidet Fehler durch zwei Pakete, deren Deklarationen sich widersprechen, deshalb schaltet tsc --init es ein. Der Preis: Ein Fehler in deinen eigenen .d.ts-Dateien wird ebenfalls nicht gemeldet.
Häufig gestellte Fragen
Was ist eine .d.ts-Datei in TypeScript?
Eine Deklarationsdatei: Sie enthält nur Typen (Funktionssignaturen, Interfaces, Klassenformen) und keine Implementierung. Sie beschreibt JavaScript, das woanders existiert, etwa eine kompilierte Bibliothek oder die eingebauten APIs des Browsers, damit TypeScript Code prüfen kann, der es nutzt. tsc erzeugt dafür nie JavaScript.
Was macht das Schlüsselwort declare in TypeScript?
Es sagt dem Compiler, dass ein Wert zur Laufzeit existiert, ohne ihn zu erzeugen. declare const VERSION: string; lässt dich VERSION als String verwenden, und die Zeile verschwindet aus der Ausgabe. Definiert nichts tatsächlich VERSION, scheitert das Programm zur Laufzeit mit einem ReferenceError.
Wie behebe ich "Could not find a declaration file for module"?
Der Fehler TS7016 bedeutet, dass das Paket keine Typen hat. Installiere sie, falls es welche gibt (npm i -D @types/package-name), oder lege eine .d.ts-Datei mit declare module "package-name" { ... } an, die beschreibt, was du nutzt. declare module "package-name"; allein bringt den Fehler zum Schweigen, indem es das ganze Modul als any typisiert.
Wie erzeuge ich .d.ts-Dateien aus TypeScript?
Setze "declaration": true in tsconfig.json (oder übergib --declaration). Jede .ts-Datei erzeugt dann eine .d.ts neben ihrer .js. Ergänze emitDeclarationOnly, wenn ein anderes Tool das JavaScript baut, und declarationMap, damit Editoren von den Typen zu deinem Quellcode springen können.
Was ist der Unterschied zwischen declare global und declare module?
declare global { ... } ergänzt den globalen Scope, zum Beispiel um eine neue Eigenschaft auf Array oder eine globale Variable. declare module "name" { ... } beschreibt ein Modul, das du unter diesem Namen importierst. declare global muss in einem Modul stehen (einer Datei mit import oder export); declare module deklariert in einer Datei ohne Imports oder Exports ein neues Modul und erweitert innerhalb eines Moduls ein bestehendes.