TypeScript-Bibliothek mit Typdeklarationen schreiben
1. Übersicht
Nutzer einer TypeScript-Bibliothek brauchen zwei Dinge: JavaScript-Code zum Ausführen und eine Type-Declaration-Datei (.d.ts), damit der Editor Vorschläge machen und der Compiler Fehler fangen kann. Falls .d.ts fehlt oder package.json auf die falsche Datei zeigt, bekommen Nutzer überall any oder den Fehler Could not find a declaration file for module.
In diesem Artikel bauen wir eine kleine Bibliothek namens order-pricing, die Bestellkosten berechnet. Hinterher wissen Sie, wie man tsconfig.json konfiguriert, um .d.ts zu generieren, exports in package.json deklariert, Type-Deklarationen manuell für alten JavaScript-Code schreibt und sowohl Runtime als auch Types vor dem Veröffentlichen testet.
2. Projekt initialisieren
Sie erstellen ein Verzeichnis und installieren die benötigten Tools:
mkdir order-pricing && cd order-pricing
npm init -y
npm install --save-dev typescript vitest
typescript gehört nur in devDependencies. Nutzer der Bibliothek brauchen den Compiler nicht, sie nutzen nur die bereits erstellten .js und .d.ts Dateien.
2.1. tsconfig.json konfigurieren
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"rootDir": "src",
"outDir": "dist",
"isolatedDeclarations": true
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
Was die wichtigsten Optionen bedeuten:
declaration: trueweisttscan, neben jeder.jsDatei auch eine.d.tsDatei zu generieren. Das ist die Kernfunktion dieses Artikels.declarationMap: truegeneriert.d.ts.mapDateien. Wenn ein Nutzer “Go to Definition” klickt, öffnet der Editor die ursprüngliche.tsDatei statt der.d.tsDatei. Damit das funktioniert, müssen Sie dassrcVerzeichnis zusammen mit der Bibliothek veröffentlichen.moduleundmoduleResolutionaufNodeNextsetzen, damit TypeScript Module genauso auflöst wie Node.js. Eine Bibliothek, die so gebaut wird, funktioniert in Bundlern und Node.js gleichermaßen.isolatedDeclarations(seit TypeScript 5.5) erzwingt, dass alle exportierten APIs eindeutig getypt werden, z.B. der Rückgabetyp einer Funktion. Dadurch können andere Tools.d.tsgenerieren, ohne den Type Checker auszuführen. Öffentliche APIs werden auch transparenter.excludeschließt Test-Dateien aus dem Build aus, damit sie nicht indistlanden.
2.2. package.json konfigurieren
{
"name": "order-pricing",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "src"],
"scripts": {
"build": "tsc",
"test": "vitest run",
"prepublishOnly": "npm run test && npm run build"
}
}
Wichtige Punkte:
"type": "module"lässt Node die.jsDateien indistals ESM interpretieren.- Im
exportsFeld muss die Bedingungtypeszuerst stehen. TypeScript durchläuft die Bedingungen der Reihe nach und stoppt bei der ersten Übereinstimmung. Stehtdefaultzuerst, erreicht TypeScripttypesnie. mainundtypesauf oberster Ebene sind ein Fallback für ältere Tools, dieexportsnicht lesen.filesbegrenzt, was auf npm hochgeladen wird. Ohne dieses Feld veröffentlichen Sie versehentlichnode_modulesoder interne Konfigurationsdateien.
3. Bibliotheks-Code schreiben
3.1. Datentypen definieren
// src/types.ts
export type CustomerTier = "regular" | "silver" | "gold";
export interface Customer {
id: string;
name: string;
tier: CustomerTier;
}
export interface OrderItem {
sku: string;
unitPrice: number;
quantity: number;
}
export interface Order {
id: string;
customer: Customer;
items: readonly OrderItem[];
}
items ist als readonly OrderItem[] deklariert. Dadurch können Nutzer normale Arrays und readonly Arrays übergeben. Die Bibliothek verpflichtet sich auch, das Array des Nutzers nicht zu verändern.
3.2. Preislogik
// src/pricing.ts
import type { CustomerTier, Order } from "./types.js";
const DISCOUNT_RATE: Record<CustomerTier, number> = {
regular: 0,
silver: 0.05,
gold: 0.1,
};
/** Gesamtbetrag vor Rabatt, Einheit VND. */
export function calculateSubtotal(order: Order): number {
return order.items.reduce(
(sum, item) => sum + item.unitPrice * item.quantity,
0,
);
}
/** Gesamtbetrag nach Anwendung des Rabatts nach Kundenstatus. */
export function calculateTotal(order: Order): number {
const subtotal = calculateSubtotal(order);
const discount = subtotal * DISCOUNT_RATE[order.customer.tier];
return Math.round(subtotal - discount);
}
Im obigen Code:
import typeteilt dem Compiler mit, dass dies nur ein Type Import ist. Diese Zeile wird nach dem Build vollständig aus der.jsDatei entfernt.- Der Import-Pfad ist
./types.js, obwohl die Quelletypes.tsist. MitNodeNextist dies erforderlich, da Node bei der Ausführung nach.jsDateien sucht. DISCOUNT_RATEwird nicht exportiert, es ist also ein internes Detail und erscheint nicht in.d.ts.- Kommentare im
/** ... */Format bleiben in.d.tserhalten. Nutzer sehen diesen Text beim Hovern im Editor.
3.3. Einstiegspunkt
// src/index.ts
export { calculateSubtotal, calculateTotal } from "./pricing.js";
export type { Customer, CustomerTier, Order, OrderItem } from "./types.js";
index.ts definiert die öffentliche API der Bibliothek. Sie sollten auch die Datentypen exportieren. Sonst müssten Nutzer Parameters<typeof calculateTotal>[0] schreiben, nur um den Order Typ zu bekommen.
4. Build und .d.ts Dateien ansehen
npm run build
Die Struktur des dist Verzeichnisses nach dem Build:
dist/
├── index.d.ts
├── index.d.ts.map
├── index.js
├── index.js.map
├── pricing.d.ts
├── pricing.d.ts.map
├── pricing.js
├── pricing.js.map
├── types.d.ts
├── types.d.ts.map
├── types.js
└── types.js.map
Inhalt von dist/pricing.d.ts:
import type { Order } from "./types.js";
/** Gesamtbetrag vor Rabatt, Einheit VND. */
export declare function calculateSubtotal(order: Order): number;
/** Gesamtbetrag nach Anwendung des Rabatts nach Kundenstatus. */
export declare function calculateTotal(order: Order): number;
//# sourceMappingURL=pricing.d.ts.map
Diese Datei enthält nur Signaturen, keine Funktionskörper. DISCOUNT_RATE ist auch nicht vorhanden. dist/types.js ist fast leer (nur export {};), da Interfaces und Type Aliases nicht zur Runtime existieren.
Falls Sie den Rückgabetyp : number bei calculateTotal vergessen, wird die isolatedDeclarations Option einen Fehler melden:
src/pricing.ts(20,17): error TS9007: Function must have an explicit return type annotation with --isolatedDeclarations.
Dieser Fehler ist hilfreich. Wenn der Rückgabetyp automatisch abgeleitet wird, könnte eine kleine Änderung im Funktionskörper versehentlich die öffentliche API verändern, ohne dass Sie es bemerken.
5. Runtime und Types testen
Eine Bibliothek mit Types muss auf zwei Ebenen getestet werden: Die Funktion gibt den richtigen Wert zurück, und die Datentypen sind wie versprochen. Vitest unterstützt beides.
// src/pricing.test.ts
import { describe, expect, expectTypeOf, it } from "vitest";
import { calculateSubtotal, calculateTotal, type Order } from "./index.js";
const goldOrder: Order = {
id: "ORD-001",
customer: { id: "C-01", name: "Lan", tier: "gold" },
items: [
{ sku: "TEA-01", unitPrice: 150_000, quantity: 2 },
{ sku: "CUP-07", unitPrice: 200_000, quantity: 1 },
],
};
describe("calculateTotal", () => {
it("berechnet Gesamtbetrag vor Rabatt", () => {
expect(calculateSubtotal(goldOrder)).toBe(500_000);
});
it("gewährt Gold-Kunden 10% Rabatt", () => {
expect(calculateTotal(goldOrder)).toBe(450_000);
});
it("hat die richtige Typ-Signatur wie versprochen", () => {
expectTypeOf(calculateTotal).parameter(0).toEqualTypeOf<Order>();
expectTypeOf(calculateTotal).returns.toBeNumber();
});
it("lehnt ungültige Kundenklassifizierungen ab", () => {
// @ts-expect-error "diamond" ist nicht in CustomerTier
const tier: Order["customer"]["tier"] = "diamond";
expect(tier).toBe("diamond");
});
});
Analyse jedes Tests:
- Die ersten beiden Tests prüfen die Logik: 2 × 150.000 + 200.000 = 500.000, nach 10% Rabatt 450.000.
expectTypeOfsperrt die Funktions-Signatur. Wenn jemand später den Rückgabetyp zustringändert, schlägt dieser Test fehl.@ts-expect-errorbehauptet, dass die folgende Zeile einen Type-Fehler verursachen muss. Falls eines Tages"diamond"gültig wird, wird TypeScript dieser Direktive einen Fehler melden.
Tests ausführen:
npm test
✓ src/pricing.test.ts (4 tests) 3ms
Test Files 1 passed (1)
Tests 4 passed (4)
Hinweis: Vitest transpiliert standardmäßig nur Code, führt aber keine Type-Überprüfung durch.
@ts-expect-errorund Type-Fehler werden nur geprüft, wenn Sietsc --noEmit -p tsconfig.test.jsonausführen (eine separate Konfiguration mit Test-Dateien) odervitest --typecheck. Sie sollten diesen Schritt in Ihre CI aufnehmen.
5.1. Paket vor Veröffentlichung prüfen
npm pack --dry-run
npm notice 📦 order-pricing@1.0.0
npm notice Tarball Contents
npm notice 1.1kB dist/index.d.ts
npm notice 312B dist/index.js
...
npm notice 945B src/pricing.ts
npm notice 402B src/types.ts
Lesen Sie diese Liste, um sicherzustellen, dass alle .d.ts Dateien vorhanden sind und keine Test-Dateien oder Geheimnisse enthalten sind. Der realistischste Weg zu testen ist, npm pack auszuführen, die .tgz Datei in ein Testprojekt zu installieren und zu importieren. Das Tool @arethetypeswrong/cli (Befehl attw --pack) findet auch die meisten exports und types Konfigurationsfehler.
6. Type-Deklarationen manuell für JavaScript-Code schreiben
Manchmal enthält eine Bibliothek noch alten JavaScript-Code, der nicht zu TypeScript konvertiert wurde. Sie können dafür manuell .d.ts schreiben.
// src/legacy-tax.js
export function applyVat(amount, rate = 0.08) {
return Math.round(amount * (1 + rate));
}
// src/legacy-tax.d.ts
/**
* Fügt Mehrwertsteuer zum Betrag hinzu.
* @param amount Betrag vor Steuern (VND).
* @param rate Steuersatz, Standard 0.08.
*/
export declare function applyVat(amount: number, rate?: number): number;
Dann exportieren Sie es aus index.ts wie ein normales Modul:
export { applyVat } from "./legacy-tax.js";
Hinweis:
tsckopiert nicht automatisch handgeschriebene.jsund.d.tsDateien indist. Sie haben zwei Optionen. Die erste ist,allowJs: truezu aktivieren, damittscdielegacy-tax.jsindistkopiert, und dannlegacy-tax.d.tsmit einem Script zu kopieren, z.B."build": "tsc && cp src/legacy-tax.d.ts dist/". Die zweite Option ist,allowJsmit JSDoc zu aktivieren undtscdie.d.tsaus JSDoc selbst generieren zu lassen. Dies ist ein häufiger Fehler: lokal funktioniert alles, aber nach der Veröffentlichung sagen Nutzer, dass das Modul fehlt.
Falls Sie einen Type für ein Drittanbieter-Package schreiben müssen, das keine Types hat, verwenden Sie declare module:
// src/shims/vn-currency.d.ts
declare module "vn-currency" {
export function format(amount: number): string;
}
Sie sollten dies nur im Projekt selbst tun. Falls die öffentliche API Ihrer Bibliothek auf diesem Shim angewiesen ist, sehen Nutzer ihn nicht.
7. Häufige Fehler
typessteht nachdefaultimexportsFeld: TypeScript liesttypesnie, Nutzer bekommenanyoder TS7016 Fehler.- Vergessen der
.jsErweiterung bei Imports mitNodeNext:tscmeldet TS2835 Fehler. Wenn Sie das vermeiden, indem Sie zumoduleResolution: "bundler"wechseln, funktioniert die Bibliothek vielleicht in Bundlern, aber bricht mit reinem Node. - Types aus
devDependenciesexportieren: FallsOrdereinen Type von einem Package indevDependenciesverwendet, wird.d.tsdieses Package importieren. Nutzer installieren es nicht, daher bekommen Sie Type-Fehler. Alle Types in der öffentlichen API müssen independenciesoderpeerDependenciessein. - Sich auf
skipLibCheckverlassen: Diese Option macht Ihren Build schneller, verbirgt aber auch Fehler in den.d.tsDateien, die Sie veröffentlichen. Testen Sie mit einem Projekt beiskipLibCheck: false. enumodernamespacebeliebig exportieren:const enummacht Probleme für Nutzer mitisolatedModules. Union-Strings wieCustomerTiersind normalerweise einfacher und sicherer.- Den öffentlichen Typ in einer Patch-Version ändern: Ein neues erforderliches Feld zu
Orderhinzufügen ist eine Breaking Change, auch wenn keine JavaScript-Zeile sich ändert. Behandeln Sie.d.tsals Teil Ihrer API bei der Versionierung.
Wann brauchen Sie nicht all das selbst zu tun? Falls Ihre Bibliothek sowohl ESM als auch CommonJS exportieren muss, verwenden Sie ein spezialisiertes Build-Tool wie tsup oder tsdown, statt tsc zweimal zu konfigurieren. Falls der Code nur intern in einem Monorepo verwendet wird, sind TypeScript project references normalerweise einfacher als die Veröffentlichung auf npm.
8. Fazit
Dieser Artikel hat die order-pricing Bibliothek von Grund auf gebaut. Hier sind die wichtigsten Schritte:
- Aktivieren Sie
declaration,declarationMapundisolatedDeclarationsintsconfig.json, um klare und stabile.d.tszu generieren. - Deklarieren Sie
exportsinpackage.jsonmittypesan erster Stelle, und begrenzen Sie die veröffentlichten Dateien mitfiles. - Testen Sie sowohl Runtime als auch Types mit Vitest,
expectTypeOfund@ts-expect-error. - Schreiben Sie
.d.tsmanuell für alten JavaScript-Code und bringen Sie sie indist.
Die wichtigste Regel ist, .d.ts Dateien als Vertrag mit Nutzern zu sehen. Prüfen Sie das Paket mit npm pack vor jeder Veröffentlichung, da viele Konfigurationsfehler nur auf der Nutzer-Seite sichtbar werden, nicht in Ihrem Projekt.