# Are the Types Wrong? Are the Types Wrong? (ATTW) is a tool for analyzing npm package contents for issues with their TypeScript types, particularly ESM-related module resolution issues. It helps package authors identify problems where their type declarations don't accurately represent the runtime JavaScript behavior, especially in Node.js's `node10`, `node16`, and `bundler` module resolution modes. The project provides both a programmatic core API (`@arethetypeswrong/core`) and a command-line interface (`@arethetypeswrong/cli`). It can detect twelve different problem types including resolution failures, module format mismatches (CJS masquerading as ESM and vice versa), incorrect default exports, missing type declarations, and internal resolution errors. The tool supports checking packages from npm, local tarballs, or directories with `npm pack` integration. ## CLI Usage ### Check Package from npm Registry The `attw` command can fetch and analyze packages directly from the npm registry using the `--from-npm` flag. ```bash # Install CLI globally npm i -g @arethetypeswrong/cli # Check a package from npm attw --from-npm axios # Check specific version attw --from-npm axios@1.4.0 # Check with version range attw --from-npm lodash@^4.0.0 # Using npx without installing npx --yes @arethetypeswrong/cli --from-npm react ``` ### Check Local Package Analyze a local tarball or directory containing a package. ```bash # Check a packed tarball npm pack attw my-package-1.0.0.tgz # Or in one command attw $(npm pack) # Pack and check a directory (will run npm pack automatically) attw --pack . # Check current directory with npx npx --yes @arethetypeswrong/cli --pack . ``` ### Output Formats Control how results are displayed using the `--format` flag. ```bash # Table format (columns = entrypoints, rows = resolution kinds) attw --from-npm axios --format table # Flipped table (columns = resolution kinds, rows = entrypoints) attw --from-npm axios --format table-flipped # ASCII format for large tables attw --from-npm vue --format ascii # Auto-detect best format for terminal width (default) attw --from-npm lodash --format auto # JSON output for programmatic use attw --from-npm axios --format json # Example JSON output structure: # { # "analysis": { # "packageName": "axios", # "packageVersion": "1.4.0", # "types": { "kind": "included" }, # "entrypoints": { ... }, # "problems": [ ... ] # }, # "problems": { # "NoResolution": [ ... ], # "UntypedResolution": [ ... ] # } # } ``` ### Entrypoint Configuration Control which package entrypoints are analyzed. ```bash # Check only specific entrypoints attw --pack . --entrypoints . one two three # Add entrypoints to auto-discovered ones attw --pack . --include-entrypoints custom-entry # Exclude specific entrypoints attw --pack . --exclude-entrypoints styles.css internal # Enable legacy mode (all files as entrypoints for packages without exports) attw --pack . --entrypoints-legacy ``` ### Analysis Profiles Profiles configure which resolution modes are required to pass. ```bash # Strict profile - requires all resolutions (default) attw --from-npm axios --profile strict # Node16 profile - ignores node10 resolution failures attw --from-npm axios --profile node16 # ESM-only profile - ignores CJS resolution failures attw --from-npm axios --profile esm-only ``` ### Ignore Specific Rules Suppress specific problem types from causing failures. ```bash # Ignore single rule attw --from-npm lodash --ignore-rules cjs-only-exports-default # Ignore multiple rules attw --from-npm axios --ignore-rules no-resolution untyped-resolution false-esm # Available rules: # no-resolution, untyped-resolution, false-cjs, false-esm, # cjs-resolves-to-esm, fallback-condition, cjs-only-exports-default, # false-export-default, unexpected-module-syntax, missing-export-equals, # internal-resolution-error, named-exports ``` ### DefinitelyTyped Integration Configure how @types packages are included for untyped packages. ```bash # Auto-infer @types version from implementation package (default) attw --from-npm lodash # Specify @types version attw --from-npm lodash --definitely-typed 4.14.0 # Disable @types inclusion attw --from-npm lodash --no-definitely-typed # Use local @types tarball attw --from-npm big.js --definitely-typed ./types-big.js-6.2.0.tgz ``` ### Configuration File Create a `.attw.json` file for persistent configuration. ```json { "format": "table", "profile": "node16", "ignoreRules": ["cjs-only-exports-default", "false-esm"], "entrypoints": [".", "./utils", "./types"], "excludeEntrypoints": ["./internal"], "emoji": true, "color": true, "summary": true } ``` ```bash # Use default config file (.attw.json) attw --pack . # Use custom config path attw --pack . --config-path ./custom-attw-config.json ``` ### Display Options Control output formatting and verbosity. ```bash # Disable emoji in output attw --from-npm axios --no-emoji # Disable colors attw --from-npm axios --no-color # Disable problem summary attw --from-npm axios --no-summary # Quiet mode (no output, only exit code) attw --from-npm axios --quiet # Show version info attw --version # Output: cli: v0.18.2 # core: v0.18.2 # typescript: v5.6.1-rc ``` ## Core API ### createPackageFromNpm Fetches a package from npm registry and returns a Package object for analysis. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; // Fetch latest version const pkg = await createPackageFromNpm("axios"); // Fetch specific version const pkgVersioned = await createPackageFromNpm("lodash@4.17.21"); // Fetch with version range const pkgRange = await createPackageFromNpm("react@^18.0.0"); // Include @types package automatically (default behavior) const pkgWithTypes = await createPackageFromNpm("lodash", { definitelyTyped: true, }); // Specify @types version explicitly const pkgWithSpecificTypes = await createPackageFromNpm("lodash", { definitelyTyped: "4.14.195", }); // Disable @types package inclusion const pkgNoTypes = await createPackageFromNpm("lodash", { definitelyTyped: false, }); // Fetch package from before a certain date const pkgBefore = await createPackageFromNpm("axios", { before: new Date("2023-01-01"), }); // Allow deprecated versions const pkgDeprecated = await createPackageFromNpm("axios@0.21.0", { allowDeprecated: true, }); console.log(`Loaded ${pkg.packageName}@${pkg.packageVersion}`); // Output: Loaded axios@1.4.0 ``` ### createPackageFromTarballData Creates a Package object from a tarball buffer (e.g., from `npm pack`). ```typescript import { createPackageFromTarballData, checkPackage } from "@arethetypeswrong/core"; import { readFileSync } from "fs"; // Read local tarball const tarballData = readFileSync("./my-package-1.0.0.tgz"); const pkg = createPackageFromTarballData(new Uint8Array(tarballData)); console.log(`Package: ${pkg.packageName}@${pkg.packageVersion}`); // Check if package contains TypeScript types if (pkg.containsTypes()) { console.log("Package includes type declarations"); } // List all files in the package const files = pkg.listFiles(); console.log("Files:", files); // Output: ["/node_modules/my-package/index.js", "/node_modules/my-package/index.d.ts", ...] // Read a specific file const packageJson = JSON.parse(pkg.readFile(`/node_modules/${pkg.packageName}/package.json`)); console.log("Main entry:", packageJson.main); // Check if file exists if (pkg.fileExists(`/node_modules/${pkg.packageName}/dist/index.d.ts`)) { console.log("Types file found"); } ``` ### createPackageFromTarballUrl Creates a Package from a remote tarball URL. ```typescript import { createPackageFromTarballUrl, checkPackage } from "@arethetypeswrong/core"; // Fetch from npm registry tarball URL const pkg = await createPackageFromTarballUrl( "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz" ); console.log(`Loaded ${pkg.packageName}@${pkg.packageVersion}`); // Output: Loaded axios@1.4.0 // Run analysis const result = await checkPackage(pkg); ``` ### checkPackage Analyzes a Package for TypeScript type issues across all module resolution modes. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; import type { Analysis, Problem, CheckResult } from "@arethetypeswrong/core"; const pkg = await createPackageFromNpm("axios"); const result: CheckResult = await checkPackage(pkg); // Check if package has types if (!result.types) { console.log(`${result.packageName}@${result.packageVersion} has no types`); process.exit(1); } // result is now Analysis type const analysis = result as Analysis; // Log detected build tools console.log("Build tools:", analysis.buildTools); // Output: { typescript: "^4.8.4", rollup: "^2.67.0" } // Iterate entrypoints for (const [subpath, entrypoint] of Object.entries(analysis.entrypoints)) { console.log(`\nEntrypoint: ${subpath}`); console.log(` Has types: ${entrypoint.hasTypes}`); console.log(` Is wildcard: ${entrypoint.isWildcard}`); // Check each resolution mode for (const [kind, resolution] of Object.entries(entrypoint.resolutions)) { console.log(` ${kind}: ${resolution.resolution?.fileName ?? "unresolved"}`); if (resolution.visibleProblems?.length) { console.log(` Problems: ${resolution.visibleProblems.length}`); } } } // Access all problems console.log(`\nTotal problems: ${analysis.problems.length}`); for (const problem of analysis.problems) { console.log(` - ${problem.kind}`); } ``` ### checkPackage with Options Configure entrypoint discovery and filtering. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; const pkg = await createPackageFromNpm("vue"); // Check only specific entrypoints const result1 = await checkPackage(pkg, { entrypoints: [".", "./jsx-runtime"], }); // Add entrypoints to auto-discovered ones const result2 = await checkPackage(pkg, { includeEntrypoints: ["./custom-entry"], }); // Exclude certain entrypoints const result3 = await checkPackage(pkg, { excludeEntrypoints: ["./macros", /internal/], }); // Enable legacy entrypoint discovery (all files as entrypoints) const result4 = await checkPackage(pkg, { entrypointsLegacy: true, }); // Combine options const result5 = await checkPackage(pkg, { includeEntrypoints: ["./extra"], excludeEntrypoints: ["./deprecated"], entrypointsLegacy: false, }); if (result5.types) { console.log(`Checked ${Object.keys(result5.entrypoints).length} entrypoints`); } ``` ### Package.mergedWithTypes Combines an implementation package with a separate types package. ```typescript import { createPackageFromNpm, createPackageFromTarballData, checkPackage, } from "@arethetypeswrong/core"; import { readFileSync } from "fs"; // Fetch implementation package without @types const implPkg = await createPackageFromNpm("lodash", { definitelyTyped: false, }); // Load custom types from local tarball const typesData = readFileSync("./my-lodash-types-1.0.0.tgz"); const typesPkg = createPackageFromTarballData(new Uint8Array(typesData)); // Merge packages const mergedPkg = implPkg.mergedWithTypes(typesPkg); console.log(`Implementation: ${mergedPkg.packageName}@${mergedPkg.packageVersion}`); console.log(`Types: ${mergedPkg.typesPackage?.packageName}@${mergedPkg.typesPackage?.packageVersion}`); // Run analysis on merged package const result = await checkPackage(mergedPkg); if (result.types?.kind === "@types") { console.log(`Using @types from: ${result.types.definitelyTypedUrl}`); } ``` ### filterProblems Filter analysis problems by kind, entrypoint, or resolution mode. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; import { filterProblems } from "@arethetypeswrong/core/problems"; import type { Analysis } from "@arethetypeswrong/core"; const pkg = await createPackageFromNpm("axios"); const result = await checkPackage(pkg); if (!result.types) { process.exit(1); } const analysis = result as Analysis; // Filter by problem kind const falseCjsProblems = filterProblems(analysis, { kind: ["FalseCJS"], }); console.log(`FalseCJS problems: ${falseCjsProblems.length}`); // Filter by entrypoint const mainEntryProblems = filterProblems(analysis, { entrypoint: ".", }); console.log(`Main entry problems: ${mainEntryProblems.length}`); // Filter by resolution kind const node16CjsProblems = filterProblems(analysis, { resolutionKind: "node16-cjs", }); console.log(`Node16 CJS problems: ${node16CjsProblems.length}`); // Filter by resolution option (covers both CJS and ESM for node16) const node16AllProblems = filterProblems(analysis, { resolutionOption: "node16", }); console.log(`All Node16 problems: ${node16AllProblems.length}`); // Combine filters const specificProblems = filterProblems(analysis, { kind: ["FalseESM", "FalseCJS"], entrypoint: ".", resolutionKind: "node16-esm", }); console.log(`Specific problems: ${specificProblems.length}`); // Alternative: pass problems array directly const customFiltered = filterProblems(analysis.problems, analysis, { kind: ["NoResolution"], }); ``` ### problemKindInfo Access metadata about each problem type including descriptions and documentation URLs. ```typescript import { problemKindInfo, allProblemKinds } from "@arethetypeswrong/core/problems"; import type { ProblemKind } from "@arethetypeswrong/core"; // List all problem kinds console.log("All problem kinds:", allProblemKinds); // Output: ["NoResolution", "UntypedResolution", "FalseCJS", "FalseESM", ...] // Get info for a specific problem const falseCjsInfo = problemKindInfo["FalseCJS"]; console.log(`${falseCjsInfo.emoji} ${falseCjsInfo.title}`); // Output: "Masquerading as CJS" console.log(falseCjsInfo.description); // Output: "Import resolved to a CommonJS type declaration file, but an ESM JavaScript file." console.log(falseCjsInfo.docsUrl); // Output: "https://github.com/arethetypeswrong/.../FalseCJS.md" // Iterate all problem types for (const kind of allProblemKinds) { const info = problemKindInfo[kind]; console.log(`${info.emoji} ${kind}: ${info.shortDescription}`); } // Output: // No Resolution // No types // Masquerading as CJS // Masquerading as ESM // ESM (dynamic import only) // Used fallback condition // CJS default export // Incorrect default export // Missing `export =` // Unexpected module syntax // Internal resolution error // Named exports ``` ### groupProblemsByKind Groups problems array by their kind for easier processing. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; import { groupProblemsByKind } from "@arethetypeswrong/core/utils"; import { problemKindInfo } from "@arethetypeswrong/core/problems"; import type { Analysis } from "@arethetypeswrong/core"; const pkg = await createPackageFromNpm("axios"); const result = await checkPackage(pkg); if (!result.types) { process.exit(1); } const analysis = result as Analysis; const grouped = groupProblemsByKind(analysis.problems); // Iterate grouped problems for (const [kind, problems] of Object.entries(grouped)) { if (!problems?.length) continue; const info = problemKindInfo[kind as keyof typeof problemKindInfo]; console.log(`\n${info.emoji} ${info.title} (${problems.length})`); for (const problem of problems) { if ("entrypoint" in problem) { console.log(` - ${problem.entrypoint} [${problem.resolutionKind}]`); } else if ("typesFileName" in problem) { console.log(` - Types: ${problem.typesFileName}`); console.log(` Impl: ${problem.implementationFileName}`); } } } ``` ### parsePackageSpec Parse npm package specifiers into structured data. ```typescript import { parsePackageSpec } from "@arethetypeswrong/core/utils"; // Parse package name only const result1 = parsePackageSpec("lodash"); console.log(result1); // { status: "success", data: { name: "lodash", versionKind: "none", version: "" } } // Parse exact version const result2 = parsePackageSpec("axios@1.4.0"); console.log(result2); // { status: "success", data: { name: "axios", versionKind: "exact", version: "1.4.0" } } // Parse version range const result3 = parsePackageSpec("react@^18.0.0"); console.log(result3); // { status: "success", data: { name: "react", versionKind: "range", version: "^18.0.0" } } // Parse dist tag const result4 = parsePackageSpec("typescript@beta"); console.log(result4); // { status: "success", data: { name: "typescript", versionKind: "tag", version: "beta" } } // Parse scoped package const result5 = parsePackageSpec("@types/node@20.0.0"); console.log(result5); // { status: "success", data: { name: "@types/node", versionKind: "exact", version: "20.0.0" } } // Handle invalid package name const result6 = parsePackageSpec("invalid name!"); if (result6.status === "error") { console.log(result6.error); // Output: "Invalid package name" } ``` ### Resolution Utilities Work with resolution modes and options. ```typescript import { allResolutionOptions, allResolutionKinds, getResolutionOption, getResolutionKinds, } from "@arethetypeswrong/core/utils"; // All resolution options (TypeScript moduleResolution settings) console.log(allResolutionOptions); // Output: ["node10", "node16", "bundler"] // All resolution kinds (includes CJS/ESM variants) console.log(allResolutionKinds); // Output: ["node10", "node16-cjs", "node16-esm", "bundler"] // Get option from kind console.log(getResolutionOption("node16-cjs")); // "node16" console.log(getResolutionOption("node16-esm")); // "node16" console.log(getResolutionOption("bundler")); // "bundler" // Get kinds from option console.log(getResolutionKinds("node10")); // ["node10"] console.log(getResolutionKinds("node16")); // ["node16-cjs", "node16-esm"] console.log(getResolutionKinds("bundler")); // ["bundler"] ``` ### visitResolutions Iterate over all entrypoint resolutions with a visitor pattern. ```typescript import { createPackageFromNpm, checkPackage } from "@arethetypeswrong/core"; import { visitResolutions } from "@arethetypeswrong/core/utils"; import type { Analysis } from "@arethetypeswrong/core"; const pkg = await createPackageFromNpm("vue"); const result = await checkPackage(pkg); if (!result.types) { process.exit(1); } const analysis = result as Analysis; // Visit all resolutions visitResolutions(analysis.entrypoints, (resolution, entrypoint) => { console.log(`${entrypoint.subpath} [${resolution.resolutionKind}]`); if (resolution.resolution) { console.log(` -> ${resolution.resolution.fileName}`); console.log(` TypeScript: ${resolution.resolution.isTypeScript}`); } else { console.log(` -> (unresolved)`); } if (resolution.visibleProblems?.length) { console.log(` Problems: ${resolution.visibleProblems.length}`); } // Return true to stop iteration early // return true; }); // Count resolutions by status let resolved = 0; let unresolved = 0; visitResolutions(analysis.entrypoints, (resolution) => { if (resolution.resolution) { resolved++; } else { unresolved++; } }); console.log(`\nResolved: ${resolved}, Unresolved: ${unresolved}`); ``` ## Summary Are the Types Wrong? is essential for npm package authors who ship TypeScript type declarations. It validates that type definitions accurately represent the runtime JavaScript across different module resolution strategies (node10 for legacy Node.js, node16 for modern Node.js with ESM support, and bundler for tools like webpack/Vite). Common use cases include CI/CD integration to catch type issues before publishing, debugging consumer reports of type/runtime mismatches, and ensuring dual CJS/ESM packages have properly configured type declarations. Integration patterns include running `attw --pack .` in pre-publish hooks, using JSON output for automated analysis in build pipelines, and leveraging the core API for custom tooling. The tool works well with package build tools like tsup, tshy, and rollup by validating their output. For packages using DefinitelyTyped, ATTW automatically fetches and validates corresponding @types packages. The exit code reflects whether problems were found, making it suitable for CI gates that enforce type correctness standards.