TypeScript Declaration Files skill for Codex

Types that match the runtime and resolve everywhere. Codex picks the right declaration form for the job (ambient module, augmentation, global, or generated), knows that one top-level import changes a .d.ts from script to module, and augments the module that really owns an interface. For published libraries it orders types first in every exports condition, ships .d.cts beside CommonJS builds, and checks the packed tarball with arethetypeswrong and publint.

  • Languages
  • $typescript-declaration-files
  • 191 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- typescript-declaration-files

Installs to ~/.agents/skills/typescript-declaration-files/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $typescript-declaration-files, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and ships TypeScript declaration files - hand-written .d.ts for untyped packages, ambient and global declarations, module augmentation for Express, Vue or environment variables, generated declarations with declaration maps and isolatedDeclarations, and publishing correct types through package.json exports checked with attw and publint. Use when a user hits "could not find a declaration file", needs to extend a third-party type, adds Window or process.env typings, or publishes a library whose types resolve wrongly for consumers.

Use it when

  • An npm package has no types and no @types package
  • A third-party interface, Window or process.env needs extra fields
  • A library you publish shows wrong or missing types for consumers
  • JavaScript sources need generated declarations

Use something else when

How to check the result

A probe file type-checks against the augmentation, and for packages attw --pack and publint report no problems on the packed tarball.

The mistake it prevents

Adding an import to a global .d.ts file, which turns it into a module and silently drops every global declaration it contained.

~/.agents/skills/typescript-declaration-files/SKILL.md

--- name: typescript-declaration-files description: Writes and ships TypeScript declaration files - hand-written .d.ts for untyped packages, ambient and global declarations, module augmentation for… license: MIT ---

Done well means: every declaration describes what the JavaScript really does at runtime, augmentations merge into the right module instead of shadowing it, and published packages resolve to the right .d.ts under every moduleResolution consumers use.

Before changing anything

  • Read AGENTS.md, every tsconfig, and package.json (type, exports, types, files).
  • Find where existing ambient files live (src/types/, types/, global.d.ts) and whether tsconfig include covers them. A .d.ts outside include is silently ignored.
  • Check whether @types/<pkg> already exists before writing anything by hand: npm view @types/<pkg> version (needs network).

Which kind of declaration

NeedWrite
Untyped npm package you importdeclare module "pkg" { ... } in a .d.ts file with no top-level import/export
Add fields to a third-party interface (Express Request, Vue ComponentCustomProperties)Module augmentation inside a module file
Add to Window, globalThis, NodeJS.ProcessEnvdeclare global { ... } inside a module file (export {})
Non-code imports (*.svg, *.module.css)Wildcard declare module "*.svg" (check the bundler’s client types first)
Types for your own JS filesallowJs + checkJs + JSDoc, emit with declaration: true
Types for a library you publishGenerate from TS sources; never hand-maintain parallel .d.ts

The rule that trips most people: a .d.ts with a top-level import or export is a module. Inside a module, declare module "pkg" augments the existing module; in a script file (no top-level import/export) it declares a new ambient module that can replace the real types.

Typing an untyped package

// src/types/legacy-chart.d.ts   (no top-level import/export)
declare module "legacy-chart" {
  export interface ChartOptions {
    width: number;
    height: number;
    theme?: "light" | "dark";
  }
  export class Chart {
    constructor(el: HTMLElement, options: ChartOptions);
    render(data: ReadonlyArray<{ x: number; y: number }>): void;
    destroy(): void;
  }
  export default Chart;
}
  • Declare only what the codebase calls; read the package source to confirm each signature.
  • Match the module format: CommonJS module.exports = fn is export = fn with import fn = require("pkg") or esModuleInterop default imports, not export default.
  • Never ship declare module "legacy-chart"; (shorthand) as a final answer: everything becomes any.

Module augmentation

// src/types/express.d.ts
import "express-serve-static-core";      // makes this file a module and targets the real one

declare module "express-serve-static-core" {
  interface Request {
    user?: { id: string; roles: string[] };
    requestId: string;
  }
}
// src/types/env.d.ts
export {};

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      NODE_ENV: "development" | "test" | "production";
      SENTRY_DSN?: string;
    }
  }
  interface Window {
    __APP_CONFIG__: { apiBase: string };
  }
}
  • Augment the module that actually declares the interface. For Express that is express-serve-static-core; for Vue it is vue (ComponentCustomProperties); for Fastify, fastify (FastifyRequest). Find it with “Go to definition” on the type.
  • You can add members to interfaces and namespaces, not replace type aliases or add new top-level exports to someone else’s module.
  • Typing process.env does not validate it. Pair it with a runtime check at startup.
  • For Vite apps, import.meta.env is augmented through interface ImportMetaEnv in vite-env.d.ts.

Generating declarations for your own code

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,          // go-to-definition jumps to .ts sources
    "emitDeclarationOnly": true,     // when a bundler emits the JS
    "isolatedDeclarations": true,    // TS 5.5+: forces explicit types on exports, enables fast parallel emit
    "outDir": "dist",
    "stripInternal": true            // drops members tagged @internal
  }
}
  • Bundled declarations: tsup --dts, tsdown, or @microsoft/api-extractor roll types into one file per entry and can report API changes.
  • For a JS codebase: tsc --allowJs --declaration --emitDeclarationOnly --outDir types src/index.js turns JSDoc into .d.ts. Fix JSDoc where output shows any.
  • Ship declarationMap files only if the .ts sources are also in the package (files), otherwise the maps point nowhere.

Publishing types correctly

{
  "name": "acme-sdk",
  "type": "module",
  "exports": {
    ".": {
      "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
      "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
    },
    "./package.json": "./package.json"
  },
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "sideEffects": false
}
  • types must come first inside each condition object; conditions match in order.
  • Each JS format needs its own declaration flavor: .d.ts next to ESM in a "type": "module" package, .d.cts next to .cjs. One .d.ts shared by both formats is the most common “masquerading as ESM/CJS” bug.
  • Top-level types is a fallback for old moduleResolution: node consumers. typesVersions is only needed to support TypeScript older than 4.7 with subpath exports.
  • Subpath exports (./utils) each need their own types entry.

Check before every release (needs network the first time to fetch the tools):

npm pack --dry-run                                   # is dist/*.d.ts actually included?
npx @arethetypeswrong/cli --pack .                   # resolution under node10, node16 CJS/ESM, bundler
npx publint                                          # exports map, files, format mismatches

Publishing to npm is outward-facing; confirm with the user before npm publish.

Contributing to DefinitelyTyped

If the package is popular and untyped, a local declare module is a stopgap. The durable fix is types in the package itself or a DefinitelyTyped PR (types/<pkg>/index.d.ts plus a <pkg>-tests.ts file). Opening an upstream PR is outward-facing; ask first.

Gotchas

  • Adding an import to a global .d.ts turns it into a module and silently disables every global it declares. Wrap them in declare global or use import("x").Type inline.
  • skipLibCheck: true (common default) skips checking your own .d.ts files too. Run a separate tsc --noEmit --skipLibCheck false -p tsconfig.types.json over hand-written ones.
  • types: ["node"] in tsconfig restricts which @types packages load globally; a new @types/* package may need adding there. TypeScript 6 and 7 start with an empty types list, so @types/node must be listed explicitly.
  • Declaration files cannot contain implementations; export const x = 1 in a .d.ts is an error. Use export const x: number;.
  • const enum in published declarations breaks consumers using isolatedModules; use regular enums or union literals.
  • Augmenting a module that is not installed creates a new ambient module instead of merging; everything appears typed but the import fails at runtime.

Verification

  • tsc --noEmit passes, and a probe file proves the augmentation is visible: const id: string = ({} as import("express").Request).requestId;
  • For packages: attw --pack . shows no red rows for the resolution modes you support; publint reports no errors.
  • Install the packed tarball into a scratch project (npm pack, then npm i ../acme-sdk-1.0.0.tgz) and import from both ESM and CJS if you ship both.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "TypeScript Declaration Files"
  short_description: "Types that match the runtime and resolve everywhere"
  default_prompt: "$typescript-declaration-files Could not find a declaration file for module legacy-chart, write types for it"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc