Type safety is a cornerstone of frontend engineering, and PDF export is no exception. dompdf.js itself is written in TypeScript, and its type definitions ship with the package, so importing it into a project gives you parameter hints and compile-time validation immediately — low-level mistakes like misspelled options or wrong field names are caught at compile time instead of in production. For many teams, though, having types is different from using them well: how do you wrap the library's option types in your own business types? How do Vue and React components stay type-aligned with the export logic? What do you do when module resolution fails? This guide starts with how to consume the shipped types, covers structuring your own export-option types, type integration in framework components, how declaration resolution works, and finishes with a tour of the most common type errors and their fixes. We also cover the hidden documentation value of types, the narrow-type discipline that keeps business layers decoupled, and the version discipline that prevents 'impossible' type errors. The goal is a type-safe, maintainable PDF export layer that stays safe as both the library and your codebase evolve. We also finish with practical guidance on reading type errors fluently and using the editor's inference panels — the skill that makes every library you adopt easier to use, and the habit that turns the compiler from an adversary into an ally.
The dompdf.js repository and its build output are TypeScript, and the published package carries its own type declarations. After installation, the editor offers full hints for the constructor options, addPage arguments, and save parameters out of the box. No @types package is needed, no local d.ts patches are required — type coverage is available from the first import, and adoption cost is effectively zero.
The direct payoff of typed hints is foolproofing: a mistyped format, a margin with a missing unit, or an addPage call missing its content argument gets flagged by the editor immediately instead of surfacing at runtime. In multi-developer projects this moves a slice of code review forward into the coding phase — errors are intercepted at their source, and review time goes to logic instead of typos.
Usage mirrors the runtime exactly: import { DomPDF } from 'dompdf.js' and the types follow automatically. Both the constructor configuration object and the per-page configuration object have explicitly constrained fields, and writing configuration gets autocompletion for free. It is the most frictionless part of adopting the library in a TS project, and new team members pick it up without any special training.
One detail worth knowing: the package exports not only the class API types but also the underlying snapshot and option types used by the documented functional API. If your project mixes both styles — class API in application code, functional API in a utilities module — the shared types keep both sides of the codebase consistent and let you migrate gradually rather than all at once.
And because the types are part of the package, upgrading dompdf.js upgrades the type surface at the same time. If a newer version renames or adds an option, the compiler tells you exactly which call sites need attention — the type system turns a library upgrade from a guessing game into a checklist.
There is also an indirect benefit that teams rarely credit: documentation. Hover hints in the editor surface parameter explanations directly in the code, effectively bringing the official documentation into every call site. New members write correct calls without opening the docs, communication costs drop, and the type system quietly becomes the most-read documentation your team has.
Using the library's option types directly works, but business projects usually want a more structured layer: consolidate the default configurations for invoices, reports, and certificates into business types with semantic field names, centralize the defaults, and let call sites pass only the differing parameters. Code intent becomes much clearer, and readability and maintainability rise together.
The wrapping pattern: define a business configuration interface holding page size, margin, and related fields, and generate DomPDF instances through a factory function. Business code depends only on its own interface, never on the library's configuration details. When a library upgrade changes the configuration shape, only the factory function needs to change — the blast radius is contained to a single point, and upgrade cost collapses.
Type alignment extends naturally to dynamic content: define a type for the export data (invoice line items, certificate fields), and let the template render function accept that type and return an HTML string. The contract between data and template is enforced by the compiler — renamed fields and missing arguments surface at compile time rather than in production. Refactoring becomes safe because the compiler watches every use site.
A discipline that pays off: keep the business types narrow. ExportConfig holding exactly the fields the feature needs — format, margin, scale — is easier to reason about and test than a mirror of the library's full option surface. Narrow types also decouple your code from library evolution: when the library grows new options, your business types do not grow unless you actually need them.
And document the contract: a short comment or README section explaining that createPdf is the only sanctioned way to build instances keeps the pattern from eroding as new developers join and 'simplify' the code into direct constructor calls scattered across the app.
Defaults deserve the same type discipline as the interface itself: define them with as const or satisfy the business type, so the compiler validates that the default values conform to the contract. A mistyped default then fails at compile time instead of surfacing at runtime — the configuration's safety boundary becomes complete, and teams can change defaults with confidence.
This example shows the complete pattern: a business ExportConfig type, a factory that merges defaults with per-call overrides, and a typed data-to-template contract. The factory is the single place where business configuration meets the library, and the typed renderInvoice function guarantees the template always receives valid data.
import { DomPDF } from 'dompdf.js';
// Business-side export configuration type
interface ExportConfig {
format: 'A4' | 'A3' | 'A5';
margin: string;
scale?: number;
}
const DEFAULTS: ExportConfig = {
format: 'A4',
margin: '20mm',
scale: 2,
};
// Factory function: business type -> DomPDF instance
function createPdf(overrides?: Partial<ExportConfig>) {
return new DomPDF({ ...DEFAULTS, ...overrides });
}
// The type contract between data and template
interface InvoiceItem { name: string; amount: number; }
function renderInvoice(items: InvoiceItem[]): string {
return items
.map(i => `<tr><td>${i.name}</td><td>${i.amount}</td></tr>`)
.join('');
}
const pdf = createPdf({ format: 'A4' });
pdf.addPage(renderInvoice([{ name: 'Consulting', amount: 8000 }]));
pdf.save('invoice.pdf');
In Vue, you obtain the container element through a ref or template ref and pass it to addPage; its type is HTMLElement, which matches the library's parameter type directly. Extract the export logic into a composable that encapsulates loading, error state, and the export call together, so components invoke a single function and both types and state management live in one place. Component code stays remarkably clean.
In React, useRef returns a ref whose current may be null, so null checks are required before calling addPage — and the types must acknowledge that possibility. Prefer putting the export logic in a custom hook with clearly typed parameters and return values; consumers get type hints for free, and error handling is centralized rather than duplicated across components. One hook, one contract, many call sites.
Whichever framework you use, the core principle is identical: isolate the business layer from the library with custom types, and give every template render function typed inputs and outputs. Framework upgrades and library upgrades then never ripple into business code, and the type system becomes the project's safety net rather than a decoration. The long-term maintenance dividend is substantial.
Two framework-specific tips: in Vue, avoid passing ref proxies where the library expects a raw element — unwrap with unref at the boundary so the type and the runtime value agree; in React, keep the export hook's dependencies stable (useCallback/useMemo) so memoized components do not re-render on every export-state change. Small discipline, noticeably smoother behavior.
And for teams with a shared component library, type the exported PDF result itself: a discriminated union of success (filename, page count, duration) and failure (code, message) makes every consumer handle both outcomes correctly — the compiler enforces what the product requirements should have said all along.
Typing the result completes the loop: components switch over an ExportResult type instead of hoping for the best, the compiler forces every branch to be handled, and the 'export failed but the UI showed nothing' class of bugs disappears from the codebase entirely. What the type system enforces, the runtime cannot betray.
After a normal npm install, module resolution finds the package's bundled type declarations automatically and imports come with types. If you hit 'cannot find module or type declaration' errors, first check that the install is complete and the build cache is fresh — usually a clean reinstall of node_modules or a cache clear resolves it. Do not rush to hand-write a d.ts override; that masks the real problem and creates a second source of truth.
When using a bundler, confirm that tsconfig's moduleResolution matches the package format: modern bundlers typically use Bundler or NodeNext resolution, and the package's exports field points them at the correct entry. A mismatch produces the strangest class of bugs — types that disagree with the actual runtime behavior — so start debugging from tsconfig before touching the bundler config.
If the project carries legacy declarations for an old dompdf.js version or a global type conflict, clean up the duplicates first and keep the package's own types. Where you genuinely need to extend the types with project-specific fields, use declaration merging with a comment explaining the reason and the version, so future maintainers do not delete it by accident. Keep the maintenance cost of custom declarations deliberately low.
One modern convenience worth mentioning: with the exports field and proper types in the package, IDE features like go-to-definition and rename work across the library boundary. That means option names can be explored from your code, not by reading the docs — which is exactly the developer experience that makes typed libraries feel effortless.
And for monorepos: if you consume dompdf.js from several packages, declare it once in the shared types package or root tsconfig and let the workspace resolve it. Re-declaring per package invites drift; a single resolution point keeps every consumer on the same type surface.
A high-frequency resolution failure deserves its own note: the IDE and the bundler sometimes read different tsconfig files, so types work in the editor but the build fails. Inherit from one root tsconfig instead of letting sub-projects configure resolution independently — consistent resolution behavior fixes the whole class of problems at once.
The most common type error is passing the wrong DOM element type: addPage expects HTMLElement, and you hand it an EventTarget or null. The fix is to narrow the type first — an instanceof check or a null guard — before the call. This satisfies the compiler and prevents runtime null dereferences at the same time, and it is the single most useful trick in framework scenarios.
Next come misspelled or mistyped configuration fields: a wrong enum value for format, or a numeric margin where the type expects a string. Thanks to the shipped types, these errors are visible in the editor immediately — fix them by following the hint, and reduce their frequency by centralizing common configurations into constants. Fewer hand-written literals means fewer chances to typo them.
Finally, async type problems: forgetting await on a Promise-returning export function, or leaving catch variables loosely typed. The disciplined approach is to give the export function an explicit return type and error type, then await and catch uniformly at call sites. The type checker flags missing awaits directly, making the async boundary safe and eliminating a whole class of runtime surprises.
A pattern that prevents several of these at once: define a single ExportResult type for the outcome of every export entry point, with success carrying the filename and failure carrying the error code. Every call site then handles both branches explicitly — the compiler enforces completeness, and the 'did the export actually succeed' question stops being a runtime mystery.
And if you find yourself fighting a type error that seems wrong, check the installed version before assuming you are at fault: a stale package with an outdated type surface explains many 'impossible' errors, and upgrading is often the entire fix. Version and type surface travel together in this library — keep both current.
Finally, learn to read type errors instead of fighting them: hover over the flagged position and compare the expected type with the actual one — the answer is usually visible at a glance. The editor's quick-fix and inference panels turn most type debugging into a two-second inspection, and reading error messages fluently is a TypeScript skill that pays off in every library you adopt.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。