Are the Types Wrong

repository·main·Indexed 23 days ago

https://github.com/arethetypeswrong/arethetypeswrong.github.io

A diagnostic tool for analyzing npm packages to identify TypeScript type and ESM module resolution issues across node10, node16, and bundler modes. It includes the @arethetypeswrong/cli for analyzing local tarballs, directories, or registry packages, @arethetypeswrong/core for programmatic analysis, and @arethetypeswrong/history for accessing historical analysis data of high-impact npm packages.

Tokens
13.6K
Snippets
24
Records
80
Agent score
82%

What's inside arethetypeswrong

  1. Overview of Are the Types Wrong?

    main

    Are the Types Wrong is a tool designed to analyze npm packages for TypeScript type issues, specifically focusing on ESM-related module resolution problems. It detects issues across node10, node16, and bundler module resolution modes.

    Key problems detected include:

    • Resolution failed: The module cannot be resolved.
    • No types: The resolution succeeds but no type definitions are found.
    • Masquerading as CJS/ESM: The package claims to be one module type but behaves like another.
    • ESM (dynamic import only): The package can only be loaded via dynamic import.
    • Used fallback condition: The package relies on problematic exports fallback conditions.
    • CJS default export issues: Problems with how default exports are handled in CommonJS.
    • Incorrect/False default exports: Mismatches in how default exports are declared vs. how they behave.
    • Missing export =: Missing the standard CommonJS export pattern.
    • Unexpected module syntax: Syntax that violates the expected module type.
    • Internal resolution error: Errors occurring during the internal resolution process.
    • Named exports: Issues specifically related to named export availability.
  2. Consequences of incomplete `export default` types

    main

    If types are incomplete (missing the export = pattern for compatibility), users may be forced to write redundant code to satisfy TypeScript, even though the runtime object is available directly on module.exports.

    Example of incorrect usage caused by bad types:

    import Whatever from "pkg";
    Whatever.default(); // This works, but `Whatever()` would have worked too!
    import Whatever from "pkg";
    Whatever.default(); // Ok, but `Whatever()` would have worked!
  3. Understand why `package.json` "exports" can cause type resolution issues

    main

    When a library uses the "exports" field in package.json, TypeScript's resolution behavior changes. In newer resolution modes, if an import resolves to an .mjs file, TypeScript expects a corresponding .d.mts declaration file.

    If a package is configured like this:

    {
      "name": "pkg",
      "main": "./index.js",
      "types": "./index.d.ts",
      "exports": {
        "import": "./index.mjs",
        "require": "./index.js"
      }
    }

    TypeScript will resolve the import to ./index.mjs but will fail to find the types because it is looking for ./index.d.mts. The top-level "types": "./index.d.ts" does not act as a fallback for "exports" entries to prevent "masquerading as CJS" (where an ESM file is incorrectly described by a CJS declaration file).

  4. Fix 'Masquerading as CJS' by providing dual declaration files

    main

    To resolve this error, ensure that your package.json exports map provides specific type declarations for both ESM and CJS entry points. A single .d.ts file cannot represent two different module formats.

    Map the import condition to a .d.mts file and the require condition to a .d.ts file:

    {
      "name": "pkg",
      "exports": {
        ".": {
          "import": {
            "types": "./index.d.mts",
            "default": "./index.mjs"
          },
          "require": {
            "types": "./index.d.ts",
            "default": "./index.js"
          }
        }
      }
    }

    Alternative Fix: Extension Substitution

    If your files follow standard naming conventions, you can simplify the exports map and let TypeScript find the correct types via extension substitution:

    {
      "name": "pkg",
      "exports": {
        ".": {
          "import": "./index.mjs",
          "require": "./index.js"
        }
      }
    }

    In this setup, TypeScript will automatically look for ./index.d.mts for imports and ./index.d.ts for requires.

  5. Handle CJS default export compatibility issues

    main

    When a CommonJS (CJS) module simulates a default export using exports.default and exports.__esModule = true without also assigning to module.exports, it creates a divergence in behavior between Node.js and bundlers (like Webpack or esbuild).

    The Problem

    If a module is structured like this:

    Object.defineProperty(exports, "__esModule", { value: true });
    exports.default = function f() {
      /* ... */
    };

    A default import import mod from "pkg"; will result in:

    • In Node.js: mod is the object { default: [Function: f] }.
    • In most bundlers: mod is the function [Function: f].

    This makes it difficult to write code that works consistently across both environments.

    Mitigation for Library Authors

    To ensure a default import correctly resolves to the intended function in both Node.js and bundlers, use a circular assignment pattern. Assign the intended export to module.exports, and then assign module.exports.default back to itself:

    Object.defineProperty(exports, "__esModule", { value: true });
    function f() {
      /* ... */
    }
    module.exports = f;
    module.exports.default = f;

    Consumer Workaround

    If you are consuming a package that has this issue and you are running in Node.js, you may need to access the export via the .default property on the imported module:

    import mod from "pkg";
    const actualExport = mod.default;
  6. How to export a class with additional types using `export =`

    main

    If your module exports a class but also needs to export additional types (like interfaces), use a namespace with the same name as the class to merge them. This allows the class to act as a namespace for the extra types while still being the primary export via export =.

    declare class Whatever {
      static default: typeof Whatever;
      /* ... */
    }
    declare namespace Whatever {
      export interface WhateverProps {
        /* ... */
      }
    }
    export = Whatever;
  7. Use @arethetypeswrong/history to analyze package history

    main

    The @arethetypeswrong/history package provides historical @arethetypeswrong/core analysis for every npm-high-impact package. It tracks the latest version available on the first of every month from January 2022 onwards.

    The data is provided as a large dataset (compressed for npm) and can be accessed programmatically in Node.js using two primary functions:

    1. getVersionsByDate(): Returns a mapping of dates to the packages and versions available on that date.
    2. getAllDataAsObject(): Returns the full analysis dataset as a JavaScript object, where keys are formatted as ${packageName}@${packageVersion}.

    Note: If a package does not contain types, its entry in the data object will be undefined.

    import { getAllDataAsObject, getVersionsByDate } from "@arethetypeswrong/history";
    
    const dates = await getVersionsByDate();
    const data = await getAllDataAsObject();
    
    // Example: Finding packages with specific problems on a specific date
    function getPackagesWithFalseCJSProblems(date) {
      const packages = dates[date];
      const result = [];
      for (const { packageName, packageVersion } of packages) {
        const analysis = data[`${packageName}@${packageVersion}`];
        // `analysis` is undefined if the package doesn't contain types
        if (analysis?.problems.some((p) => p.kind === "FalseESM")) {
          result.push(analysis);
        }
      }
      return result;
    }
    
    const mayFalseESMProblems = getPackagesWithFalseCJSProblems("2023-05-01").length;
    const juneFalseESMProblems = getPackagesWithFalseCJSProblems("2023-06-01").length;
    console.log({ mayFalseESMProblems, juneFalseESMProblems });