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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- typescript-declaration-files Installs to ~/.agents/skills/typescript-declaration-files/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install typescript-declaration-files The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project typescript-declaration-files Installs to .agents/skills/typescript-declaration-files/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/typescript-declaration-files.zipunzip -o typescript-declaration-files.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) typescript-declaration-files PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The broader task is moving a package from CommonJS to ESM: use node-esm-migration
- You are debugging a single compiler error in application code: use typescript-type-errors
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.
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, andpackage.json(type,exports,types,files). - Find where existing ambient files live (
src/types/,types/,global.d.ts) and whether tsconfigincludecovers them. A.d.tsoutsideincludeis silently ignored. - Check whether
@types/<pkg>already exists before writing anything by hand:npm view @types/<pkg> version(needs network).
Which kind of declaration
| Need | Write |
|---|---|
| Untyped npm package you import | declare 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.ProcessEnv | declare 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 files | allowJs + checkJs + JSDoc, emit with declaration: true |
| Types for a library you publish | Generate 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 = fnisexport = fnwithimport fn = require("pkg")oresModuleInteropdefault imports, notexport default. - Never ship
declare module "legacy-chart";(shorthand) as a final answer: everything becomesany.
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 isvue(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.envdoes not validate it. Pair it with a runtime check at startup. - For Vite apps,
import.meta.envis augmented throughinterface ImportMetaEnvinvite-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-extractorroll types into one file per entry and can report API changes. - For a JS codebase:
tsc --allowJs --declaration --emitDeclarationOnly --outDir types src/index.jsturns JSDoc into.d.ts. Fix JSDoc where output showsany. - Ship
declarationMapfiles only if the.tssources 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
}
typesmust come first inside each condition object; conditions match in order.- Each JS format needs its own declaration flavor:
.d.tsnext to ESM in a"type": "module"package,.d.ctsnext to.cjs. One.d.tsshared by both formats is the most common “masquerading as ESM/CJS” bug. - Top-level
typesis a fallback for oldmoduleResolution: nodeconsumers.typesVersionsis only needed to support TypeScript older than 4.7 with subpath exports. - Subpath exports (
./utils) each need their owntypesentry.
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
importto a global.d.tsturns it into a module and silently disables every global it declares. Wrap them indeclare globalor useimport("x").Typeinline. skipLibCheck: true(common default) skips checking your own.d.tsfiles too. Run a separatetsc --noEmit --skipLibCheck false -p tsconfig.types.jsonover hand-written ones.types: ["node"]in tsconfig restricts which@typespackages load globally; a new@types/*package may need adding there. TypeScript 6 and 7 start with an emptytypeslist, so@types/nodemust be listed explicitly.- Declaration files cannot contain implementations;
export const x = 1in a.d.tsis an error. Useexport const x: number;. const enumin published declarations breaks consumers usingisolatedModules; 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 --noEmitpasses, 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;publintreports no errors. - Install the packed tarball into a scratch project (
npm pack, thennpm 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.yaml
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.