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:

src/types/pompelmi.d.ts
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)}`);
    }
  }
}
The 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