Complete TypeScript integration guide for pompelmi
pompelmi is written in JavaScript and does not ship with TypeScript
declaration files. This guide shows you how to add full type coverage with a
local .d.ts file, how to write typed verdict helpers that narrow
correctly, and how the integration looks in common TypeScript frameworks.
Declaration file
Create a file that TypeScript will pick up automatically when you
import or require pompelmi:
declare module 'pompelmi' {
/** Options for the TCP scan path (clamd over the network). */
interface ScanOptions {
/** Hostname or IP of the clamd daemon. Default: '127.0.0.1' */
host?: string;
/** TCP port clamd listens on. Default: 3310 */
port?: number;
/** Socket idle timeout in ms (TCP path only). Default: 15000 */
timeout?: number;
}
/**
* Frozen object of Symbol constants representing scan outcomes.
* Always compare with === against these constants — never against strings.
*/
const Verdict: Readonly<{
/** No threats detected. Safe to proceed. */
Clean: unique symbol;
/** A malware signature was matched. Reject the file. */
Malicious: unique symbol;
/** Scan ran but could not complete. Treat as untrusted. */
ScanError: unique symbol;
}>;
/** Union type of all possible scan verdicts. */
type ScanVerdict = typeof Verdict[keyof typeof Verdict];
/**
* Scan a file for malware.
* @param filePath Absolute path to the file on disk.
* @param options Optional TCP options. Omit to use local clamscan CLI.
* @returns A Promise that resolves to a ScanVerdict Symbol.
*/
function scan(filePath: string, options?: ScanOptions): Promise<ScanVerdict>;
export { scan, Verdict, ScanVerdict, ScanOptions };
}
tsconfig.json setup
Ensure TypeScript picks up the declaration file. There are two ways:
Option A — include path
// tsconfig.json
{
"compilerOptions": { ... },
"include": ["src/**/*"] // already picks up src/types/pompelmi.d.ts
}
Option B — typeRoots
{
"compilerOptions": {
"typeRoots": ["./src/types", "./node_modules/@types"]
}
}
Verify resolution is working:
import { scan, Verdict } from 'pompelmi';
// ^^ should auto-complete in your IDE
Type narrowing with switch
Because Verdict members are unique symbol types,
TypeScript's control-flow analysis narrows correctly in a switch
statement:
import { scan, Verdict, ScanVerdict } from 'pompelmi';
async function handleVerdict(filePath: string): Promise<void> {
const result: ScanVerdict = await scan(filePath);
switch (result) {
case Verdict.Clean:
// TypeScript knows: result is typeof Verdict.Clean here
console.log('Safe to proceed.');
break;
case Verdict.Malicious:
console.log('Malware detected.');
break;
case Verdict.ScanError:
console.log('Scan failed — treating as unsafe.');
break;
default: {
// Exhaustiveness check: this line should never be reached.
// If a new Verdict is added without updating this switch,
// TypeScript will flag _never as 'never' type.
const _never: never = result;
throw new Error(`Unhandled verdict: ${String(_never)}`);
}
}
}
default: const _never: never = result pattern is an
exhaustiveness check. If you ever add a new Verdict to the declaration file
without handling it in the switch, TypeScript will fail to compile with a
type error — a useful safety net.
Typed verdict helpers
Helper functions let you convert verdicts to strings for logging, HTTP status codes, or database storage:
// src/lib/verdict.ts
import { Verdict, ScanVerdict } from 'pompelmi';
export function verdictLabel(v: ScanVerdict): string {
switch (v) {
case Verdict.Clean: return 'clean';
case Verdict.Malicious: return 'malicious';
case Verdict.ScanError: return 'scan_error';
}
}
export function verdictHttpStatus(v: ScanVerdict): number {
switch (v) {
case Verdict.Clean: return 200;
case Verdict.Malicious: return 400;
case Verdict.ScanError: return 422;
}
}
export function isRejected(v: ScanVerdict): boolean {
return v === Verdict.Malicious || v === Verdict.ScanError;
}
ESM (ES Modules) usage
pompelmi uses CommonJS. If your project uses ESM
("type": "module" in package.json) you can still
import it. Node.js supports importing CommonJS modules from ESM:
// ✓ Works in ESM projects
import { scan, Verdict } from 'pompelmi';
If you use a bundler (esbuild, Rollup, Webpack) or a meta-framework that
handles the CJS/ESM boundary, the import works as written above. For bare
Node.js ESM with --input-type=module, the same syntax works
because Node.js wraps CJS exports in a default export with named re-exports.
NestJS typed service
With the declaration file in place, the NestJS scan service has full type coverage without any casts:
// scan/scan.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { scan, Verdict, ScanVerdict } from 'pompelmi';
@Injectable()
export class ScanService {
async scanFile(filePath: string): Promise<ScanVerdict> {
try {
return await scan(filePath);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new InternalServerErrorException('ClamAV unavailable: ' + message);
}
}
isClean(verdict: ScanVerdict): boolean {
return verdict === Verdict.Clean;
}
isMalicious(verdict: ScanVerdict): boolean {
return verdict === Verdict.Malicious;
}
}
The full NestJS interceptor and module setup is covered in the dedicated guide: How to use pompelmi in a NestJS application.
Next steps
-
Full API reference for
scan()andVerdict? See the API Reference. - NestJS integration? See How to use pompelmi in a NestJS application.
- Next.js App Router integration? See Scanning file uploads in Next.js (App Router).