Senior 10 min · March 06, 2026

TypeScript .d.ts - Hand-Maintained Files Break Monorepo

After updating a shared npm package, hand-maintained .

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • .d.ts files describe JavaScript library shapes without containing runtime code
  • TypeScript uses module resolution to find matching .d.ts files
  • Hand-authored .d.ts files use declare module, namespace, and interface
  • Production pitfall: mismatched version between .d.ts and actual implementation causes 'any' types
  • Biggest mistake: adding .d.ts files without ensuring they match the JS module name exactly
Plain-English First

Imagine you buy a brand-new kitchen appliance that came from Japan. The appliance itself works perfectly, but all the instructions are in Japanese. A declaration file is like a professional translator who writes you a perfect English manual — they don't change the appliance at all, they just tell you what every button does. TypeScript declaration files do exactly that for JavaScript libraries: they describe the shape of existing JavaScript code so TypeScript knows how to work with it, without touching the original code.

Every serious TypeScript project eventually hits the same wall: you install a popular npm package, write an import statement, and TypeScript throws a red underline at you saying it 'cannot find module' or has 'no type information'. That's not a bug in your code — it's TypeScript being honest. It doesn't know the shape of that JavaScript library, so it refuses to guess. Declaration files are the solution TypeScript ships with to bridge the 30-year gap between untyped JavaScript and the fully-typed world TypeScript promises.

The problem declaration files solve is deceptively simple but architecturally profound. TypeScript needs to perform static analysis at compile time, before a single line of JavaScript runs. When you consume a library written in plain JavaScript, there's nothing to analyze — no type annotations, no interfaces, just dynamic runtime behavior. Declaration files are a parallel universe of pure type information: they mirror every exported function, class, object, and constant from a JavaScript file, describing their shapes without containing any executable code whatsoever. The compiler reads the .d.ts file; the browser or Node.js runs the .js file. They're two different files doing two completely different jobs.

By the end of this article you'll understand how the TypeScript compiler resolves declaration files using module resolution strategies, how to author hand-crafted .d.ts files for legacy JavaScript that has no types, how to publish a typed library correctly so consumers get a great autocomplete experience, and exactly where the sharp edges are — the mistakes that waste entire afternoons in production codebases. We're going deep on internals, not skimming the surface.

What Are TypeScript Declaration Files?

A .d.ts file is a TypeScript declaration file — it describes the shape of existing JavaScript code without containing any executable logic. Think of it as a type-level mirror of a JavaScript module. TypeScript reads these files during compilation to understand the types of external libraries, and they're stripped entirely from the output JavaScript. They only exist at compile time.

Declaration files come in three flavors
  • Bundled: If a npm package is written in TypeScript and compiled with the --declaration flag, .d.ts files are generated alongside the .js files. This is the gold standard.
  • DefinitelyTyped: Community-maintained type packages under @types/ scope. For example, @types/lodash provides .d.ts files for Lodash.
  • Hand-authored: When no existing types exist, you write them yourself. This is common for internal proprietary libraries or old jQuery plugins.

The type system inside a .d.ts file is exactly the same as in regular TypeScript, but with one constraint: you cannot put runtime code. No console.log, no assignments, no side effects. Only declare, interface, type, function, class, namespace, module — all the keywords that define structures, not implementations.

io/thecodeforge/example.d.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io/thecodeforge/example.d.ts
// Declaration file for a JavaScript utility library
declare namespace io.thecodeforge {
    function formatUserName(first: string, last: string): string;
    interface UserConfig {
        maxRetries: number;
        timeoutMs: number;
    }
    const DEFAULT_CONFIG: UserConfig;
}

export = io.thecodeforge;
Why This Matters
If you ever see a red squiggly under an import statement, it's because TypeScript can't find the matching .d.ts. That's the first clue that module resolution is failing.
Production Insight
A single missing .d.ts file can cascade into 'any' types throughout your codebase.
TypeScript falls back to 'any' when it can't find declarations — that defeats the purpose of using TypeScript.
Rule: always track whether each dependency has bundled types or needs @types/.
Key Takeaway
.d.ts files are type-only mirrors of JavaScript modules.
They contain no runtime code — only shape descriptions.
If you can't import a module, start by checking its declaration file.
How to Get Types for a Library
IfLibrary is written in TypeScript and publishes .d.ts
UseNo action needed — types are bundled. Just install the package.
IfLibrary is JavaScript but has @types/ on npm
UseInstall @types/library-name as a dev dependency.
IfNo types exist, library is small and stable
UseWrite a hand-authored declaration file using declare module.
IfNo types exist, library is large and changes often
UseUse a community types package or consider writing a type abstraction layer.

Declaration File Structure Decision Tree — Which Type Source to Use

Choosing the right source of type declarations for a library is a decision that directly affects maintenance burden and developer experience. This decision tree walks through the common scenarios and helps you pick the best strategy based on the library's origin, popularity, and stability.

Start by asking: Is the library written in TypeScript? If yes and it publishes its own .d.ts files (via the types field in package.json), then you're done — types are bundled. If it's TypeScript but doesn't publish types, check the package.json for a types field or look for a dist/ folder with .d.ts files. If missing, consider opening an issue or contributing a PR.

If the library is plain JavaScript, move to the next question: Does the library have an @types/ package on npm? Run npm info @types/library-name or search on https://github.com/DefinitelyTyped/DefinitelyTyped. If yes, install it as a devDependency. If no, ask: Is the library popular and stable? Popular libraries with many consumers often have community-maintained types in DefinitelyTyped even if not published yet — check the DefinitelyTyped repository for open PRs. For less popular or internal libraries, you have two options: write a hand-authored .d.ts file, or use a quick ambient declaration (declare module 'lib-name';) which gives everything any type.

A hand-authored .d.ts is appropriate when the library has a small, stable API surface and you need full type safety. For large, frequently changing libraries, hand-authoring becomes a maintenance sink — consider wrapping the library behind a typed abstraction layer instead, or advocating for the library author to add types.

This decision tree helps you avoid the common mistake of over-engineering when types aren't needed, or under-investing when type safety is critical.

Don't Default to 'declare module' Without a Body
The quickest fix — declare module 'x'; — makes every export any. That silences errors but undermines type safety. Reserve it for truly temporary workarounds.
Production Insight
In a monorepo, the decision tree often differs per package. A shared utility library with a small API can use hand-authored .d.ts, while a large third-party dependency should rely on @types/. Use a consistent policy across the repo and document it in your contributing guide.
Key Takeaway
Evaluate the library's origin, popularity, and API stability before deciding the type source. Hand-authored files are a last resort for small, stable APIs — never for large, volatile libraries.

@types/ Package Reference — How to Find and Contribute to DefinitelyTyped

DefinitelyTyped is the community-driven repository that hosts thousands of type definitions for JavaScript libraries. When you run npm install @types/lodash, you're downloading a package automatically published from the DefinitelyTyped GitHub repository. Understanding how to scan for available @types packages and how to contribute when a package is missing is essential for any production TypeScript project.

Finding available @types packages: - Use npm search @types/<package-name> to check if a types package exists. - Visit https://www.npmjs.com and search for @types/package-name. - Use the DefinitelyTyped repository: https://github.com/DefinitelyTyped/DefinitelyTyped — browse the types/ directory. - Use CLI: npm info @types/library-name versions to see available versions (match the library version).

When to install @types: - For any runtime dependency that is JavaScript and has a corresponding @types package, install it as a devDependency. - Keep the version of @types aligned with the library's version. Use tilde or exact version range to avoid breaking changes. - Never commit generated files from @types — they are managed by npm.

Contributing to DefinitelyTyped: If a library lacks an @types package, you can contribute one. The process: 1. Fork the DefinitelyTyped repo. 2. Create a new directory under types/<package-name>/. 3. Write an index.d.ts file with the library's type declarations. 4. Add a tsconfig.json and tester.ts file (see existing packages for templates). 5. Run npm test to verify. 6. Submit a pull request. The DT maintainers will review and, if accepted, publish the package automatically.

Common pitfalls with @types: - Version mismatches: If the library is on v2 but @types is on v1, you'll get type errors for APIs that changed. Always pin both versions. - Overlapping types: Some libraries (like React) have both bundled types (when using @types/react) and additional community types — scope your installation carefully. - Mocking @types in test environments: If you use @types/node, ensure your test runner's Node.js version matches the type definitions.

package.json (consumer side)JSON
1
2
3
4
5
6
7
8
{
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "@types/lodash": "^4.17.14"
  }
}
Use npm info to find compatible versions
Run npm info @types/lodash versions and compare with the lodash version you're using. For lodash 4.17.21, you might need @types/lodash 4.17.14 — the major/minor should match.
Production Insight
A stale @types package is a silent type-safety leak. After upgrading a library, always update its @types counterpart in the same commit. Automate this with Dependabot configured to ignore runtime-only upgrades when @types is updated separately.
Key Takeaway
Always check for an @types package before hand-authoring .d.ts. Contribute missing types to DefinitelyTyped when feasible — it benefits the entire ecosystem.

Module Resolution: How TypeScript Finds .d.ts Files

TypeScript's module resolution is the algorithm that maps import specifiers to files on disk. It's roughly: for import { something } from 'my-lib', TypeScript looks for a .ts, .tsx, or .d.ts file at the resolved path. The resolution strategy (classic vs node) determines exactly where it searches.

In practice, you'll almost always use the node resolution strategy (the default when module is 'commonjs' or 'node16'). Here's the order: 1. Check if the specifier is a relative path (starts with './' or '../'). If so, look for the file directly. 2. For non-relative module names (like 'lodash'), TypeScript searches through the node_modules directories up the directory tree. 3. Inside a package's node_modules, TypeScript looks at the package's types field in package.json (or typing as fallback). 4. If no types field, it looks for index.d.ts at the package root. 5. If still not found, it searches for @types/package-name in node_modules/@types/. 6. If nothing is found — you get the 'Cannot find module' error.

One critical detail: TypeScript also respects typeRoots and types in tsconfig.json. You can override the default search paths, especially in monorepo setups where you need to point to shared types.

tsconfig.jsonJSON
1
2
3
4
5
6
7
{
  "compilerOptions": {
    "moduleResolution": "node",
    "typeRoots": ["./io/thecodeforge/types", "./node_modules/@types"],
    "types": ["jest", "node"]
  }
}
Undefined TypeRoots Trap
If you set typeRoots manually, TypeScript stops looking in node_modules/@types unless you explicitly include that path. This is a common source of CI vs local inconsistencies.
Production Insight
Monorepos with hoisted dependencies often confuse module resolution.
Symlinks in node_modules can cause TypeScript to resolve to the wrong version of a .d.ts.
Rule: always run tsc --traceResolution when module resolution behaves differently than expected.
Key Takeaway
Module resolution follows a fixed priority: types field > index.d.ts > @types/.
Custom typeRoots override defaults — include both your custom paths and the original @types path.
Use --traceResolution to debug which file TypeScript actually picks.

Authoring Hand-Crafted .d.ts Files

When you're stuck with a dependency that has no types and no @types/ package, you write the declaration file yourself. This is surprisingly common in corporate environments with legacy internal libraries.

The starting point is a declare module block. The module name must exactly match the import specifier. For example, if you import import { parse } from 'my-old-parser', your .d.ts must start with declare module 'my-old-parser' { ... }.

Inside the module, you can export interfaces, types, functions, classes, and variables. Use declare function for standalone functions. Use namespace to group related types. Don't forget to export the module's default export if it uses CommonJS module.exports.

A common pattern is to create a globals.d.ts file in your project root and declare all missing modules there. But be careful: if two declarations with the same module name exist, TypeScript merges them — which can lead to unexpected conflicts.

Pro tip: Use declare module 'some-module' without any block to immediately suppress the 'cannot find module' error. This is the quickest way to silence TypeScript, but it's also the most dangerous because it makes everything any.

io/thecodeforge/types/legacy-parser.d.tsTYPESCRIPT
1
2
3
4
5
6
7
8
// io/thecodeforge/types/legacy-parser.d.ts
declare module 'my-old-parser' {
    export function parse(input: string): Record<string, unknown>;
    export type ParseError = { message: string; line: number };
    export function formatError(err: ParseError): string;
}

// Also works with a namespace: declare namespace io.thecodeforge.LegacyParser {}
Think of .d.ts as a TypeScript version of the library's API documentation
  • Every export in the .d.ts must have a corresponding runtime export in the JavaScript file.
  • You don't need to declare internal variables or helper functions — only the public API surface.
  • Ambient declarations (like interface Window) are global and don't need a module wrapper.
  • Use export default carefully: CommonJS interop can behave differently in ES module consumers.
Production Insight
Hand-authored .d.ts files drift from the actual code over time.
The most common failure: adding a new API to the JS file but forgetting to update the .d.ts.
Rule: if you maintain a hand-authored .d.ts, set up a CI job that runs tsc against the declaration and the actual JS to spot mismatches.
Key Takeaway
Use declare module 'module-name' to enclose all declarations.
The module name must match the import specifier exactly.
Never rely solely on ambient declarations — they pollute the global scope.

Extending Global Objects (Window/Request) — The 'declare global' Pattern

Sometimes you need to add custom properties to global objects like Window, Request, NodeJS.Process, or any built-in interface. TypeScript provides the declare global block to safely augment these global namespaces from within a module file. This is essential when you're writing polyfills, feature flags stored on window, or custom request properties in a Node.js server.

The pattern: Inside a module (any file with at least one import or export), use declare global { ... } to add members to global interfaces. For example, to add a custom appConfig property to Window:

```typescript // src/global.d.ts or any module .ts file export {}; // makes this a module

declare global { interface Window { appConfig: { apiUrl: string; theme: 'dark' | 'light' }; } } ```

Now TypeScript recognizes window.appConfig anywhere in your project without additional type assertions.

Why not use a non-module ambient declaration? If you write interface Window { ... } in a non-module .d.ts file (no imports/exports), that's a global augmentation — it's automatically in effect for all files. That can cause conflicts if two packages attempt to augment the same interface differently. By using declare global inside a module, the augmentation is only active in files that import the module (or if the module is included via tsconfig). This gives you control and reduces namespace pollution.

Common use cases: - Adding custom properties to Window for frontend frameworks. - Extending Request in Express or Next.js to include user or session. - Adding methods to Array, String, or other built-in prototypes (though this is generally discouraged). - Augmenting NodeJS.ProcessEnv to include custom environment variables.

Best practices: - Keep global augmentations in a single .d.ts file, imported indirectly via your project's entry point. - Use export {} to ensure the file is treated as a module if it contains only declare global. - Prefer module augmentation over global augmentation when possible: declare module 'express' { interface Request { user: User } } is scoped to that module. - Document each augmentation and why it's needed — future developers will thank you.

src/global.d.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/global.d.ts
// This file augments global types for the entire project

export {}; // mark as a module

declare global {
  interface Window {
    __APP_CONFIG__: {
      GA_TRACKING_ID: string;
      FEATURE_FLAGS: Record<string, boolean>;
    };
  }

  // Augment the Request type from Express (if using @types/express)
  namespace Express {
    interface Request {
      user?: {
        id: string;
        roles: string[];
      };
    }
  }
}
Global Augmentation vs Module Augmentation
Use declare global for built-in browser/Node globals (Window, Request, Process). Use module augmentation (declare module 'express' { ... }) for third-party library types. The latter is safer because it's scoped to that specific module.
Production Insight
Over-using global augmentation can lead to hard-to-debug type collisions when multiple packages try to add the same property. In a monorepo, centralize all global augmentations in one file and enforce code reviews for any additions. Version-control-breaking changes to global shapes with the same rigor as library APIs.
Key Takeaway
Use declare global inside a module (with export {}) to safely extend global objects. Prefer module augmentation for third-party libraries. Keep global augmentations centralized and documented.

Publishing Typed Libraries — The 'types' Field and Best Practices

If you're publishing a library, you need to decide how to provide types. If your library is written in TypeScript, the answer is simple: set declaration: true in tsconfig.json and add a types field to package.json pointing to the main .d.ts file. TypeScript will generate .d.ts alongside .js outputs.

For JavaScript-only libraries, you can either
  • Publish separate @types packages (like DefinitelyTyped).
  • Include hand-authored .d.ts files in the same package.

The types field in package.json tells TypeScript which file to load when someone imports your module. If omitted, TypeScript falls back to index.d.ts in the package root. If you have multiple entry points (like a main entry and a subpath export), you need the exports field with types condition.

Important: If you use exports in package.json, the types field is ignored for the main entry. TypeScript uses the exports map to resolve types for each subpath. This is a common source of confusion — a library may have types pointing to the right file but consumers still get 'any' types because the exports map lacks a types condition.

package.json (published library)JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "name": "@io/thecodeforge/core",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./helpers": {
      "types": "./dist/helpers/index.d.ts",
      "import": "./dist/helpers/index.mjs",
      "require": "./dist/helpers/index.js"
    }
  }
}
The exports Field Trap
When you add an exports field, every module path must be explicitly listed. If a consumer tries to import a subpath that isn't listed, Node.js (and TypeScript) will throw — even if that file exists on disk.
Production Insight
A missing types condition in exports causes TypeScript to ignore bundled .d.ts files.
Consumers get 'any' types silently — a type-safety disaster that's hard to debug from the consumer's side.
Rule: always test your published package by installing it in a fresh TypeScript project before releasing.
Key Takeaway
Set both types and exports with the types condition.
For TypeScript libraries, generate .d.ts automatically via --declaration.
Test your type definitions with a clean consumer project.

Production Pitfalls and Debugging Techniques

Even with everything set up correctly, declaration files can cause hours of debugging. Here are the most painful scenarios and how to fix them.

Pitfall 1: Triple-slash directives in .d.ts Sometimes you see /// <reference types="..." /> at the top of a .d.ts file. These are legacy directives that tell the compiler about dependencies. In modern TypeScript projects, they're rarely needed and often cause problems when files are moved or renamed. Avoid writing them — use regular imports in your .ts files instead.

Pitfall 2: Global augmentation collisions If two packages both augment the global interface (e.g., declare global { interface Window { ... } }), the second one may overwrite the first. This is a common issue in monorepos where multiple packages extend DOM types. The fix is to use module augmentation inside a module scope, not global augmentation.

Pitfall 3: Version mismatch between library and its types If your app depends on awesome-lib@1.2.0 but @types/awesome-lib@1.0.0 is installed, you get type errors that don't match the runtime behavior. Always keep the @types version in sync with the runtime version. Use npm ls @types/awesome-lib to check.

Pitfall 4: Incorrect moduleResolution setting With modern Node.js (16+) and TypeScript, using moduleResolution: "node" can produce different results than moduleResolution: "node16" or "bundler". The "bundler" option is more lenient and often used in webpack projects. If you switch between these, declarations may not resolve correctly.

io/thecodeforge/debugging/shared-resolution.d.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
// Wrong: Triple-slash directive (avoid)
/// <reference types="io.thecodeforge.core" />

// Right: Use an import (preferred)
import type { Service } from '@io/thecodeforge/core';

declare global {
    interface MyApp {
        service: Service;
    }
}
The Triple-Slash Directive is a Red Flag
If you see /// <reference in a .d.ts file that you didn't write, it's often a sign of poor tooling. Modern TypeScript prefers import type and export type. Refactor if possible.
Production Insight
Version mismatch between @types and actual library is the #1 cause of type errors in production builds.
Always use exact version pins for @types packages.
Run tsc --noEmit as part of CI — it catches these mismatches before they hit the build server.
Key Takeaway
Avoid triple-slash directives — use imports instead.
Keep @types version pinned to match the runtime library.
Use tsc --noEmit and --traceResolution to diagnose issues fast.
● Production incidentPOST-MORTEMseverity: high

The Micro-Patch That Broke Type Checking Across the Monorepo

Symptom
After updating a shared npm package, TypeScript builds started failing with 'Property 'registerHook' does not exist on type 'import("@io/thecodeforge-core").Service'.
Assumption
The team assumed that because the JavaScript code was updated and the package version bumped, TypeScript would automatically pick up the new export. They didn't check the .d.ts file.
Root cause
The library's main .d.ts file (index.d.ts) was hand-maintained and not regenerated from the TypeScript source. The developer who added registerHook only updated the JavaScript implementation, forgetting to add the type declaration. TypeScript saw the runtime code but the declaration said the function didn't exist.
Fix
1. Regenerated the .d.ts file by running tsc --declaration true --emitDeclarationOnly. 2. Added a CI step that fails if the generated .d.ts differs from the hand- authored version. 3. Published a patch version with the corrected declarations.
Key lesson
  • Always regenerate .d.ts files from TypeScript source automatically — never maintain them by hand for libraries written in TypeScript.
  • For JS libraries, use the 'types' field in package.json and test the .d.ts file by actually importing it in a strict TypeScript project.
  • Add a CI check that compiles a consumer project against the declaration file to catch mismatches before release.
Production debug guideSymptom → Action guide for the most common declaration file issues4 entries
Symptom · 01
TypeScript error: 'Cannot find module 'my-lib' or its corresponding type declarations.'
Fix
Check if 'my-lib' has a 'types' or 'typing' field in package.json. If not, you'll need to install @types/my-lib or create a local declaration file.
Symptom · 02
Imported value is 'any' even though the library clearly has runtime exports.
Fix
TypeScript is falling back to 'any' because it can't find a matching .d.ts. Run tsc --traceResolution to see which files were considered and why they were rejected.
Symptom · 03
After updating a library, existing code gets 'Property X does not exist on type Y' errors.
Fix
The .d.ts file hasn't been updated to reflect the new version. Check if the library uses a separate types package (like @types/some-lib) that needs to be updated separately.
Symptom · 04
TypeScript compiles fine locally but fails in CI with module resolution errors.
Fix
Symlinks or npm workspaces can confuse module resolution. Run tsc --listFiles in both environments and compare the resolved file lists. Also check if node_modules is installed correctly.
★ Quick Debug Cheat Sheet for .d.ts IssuesFive-minute diagnostics for the most common declaration file headaches
Module not found error
Immediate action
First run tsc --noEmit to see full error list
Commands
tsc --traceResolution
echo $NODE_PATH
Fix now
Install @types/package-name or create a manual .d.ts file with declare module 'package-name';
Unexpected 'any' type on import+
Immediate action
Check if the .d.ts file actually exports the symbol
Commands
grep 'export' node_modules/@types/package-name/*.d.ts
cat node_modules/package-name/package.json | grep types
Fix now
Add interface definition to a local declaration file or use declare module 'package-name' { export const foo: string; }
Type errors after library version bump+
Immediate action
Compare old and new .d.ts files for breaking changes
Commands
diff node_modules/old-pkg/index.d.ts node_modules/new-pkg/index.d.ts
npm ls @types/package-name
Fix now
Pin both the library and its types to compatible versions, or override the .d.ts with a local augmentation
Declaration File Sources Compared
Source TypeMaintenance BurdenBest For
Bundled (generated from TypeScript source)Low — automatically regeneratedLibraries you own and write in TypeScript
DefinitelyTyped (@types/)Medium — community maintained, version lag possiblePopular JS libraries without official types
Hand-authored .d.tsHigh — must manually keep in syncInternal proprietary libraries or untyped legacy code
Ambient declaration (declare module without body)Low — but gives all any typesQuick fix for development, never for production

Key takeaways

1
Declaration files (.d.ts) are type-only mirrors of JavaScript modules
no runtime code.
2
Module resolution uses a strict priority
types field > index.d.ts > @types/.
3
When authoring hand-crafted .d.ts files, match the module name exactly and export only the public API.
4
For published libraries, always include a types condition in the exports map
not just the legacy types field.
5
Use tsc --traceResolution to debug why TypeScript can't find or misresolves a declaration file.
6
Never rely on ambient declare module without a body
it reduces all exports to any, defeating static analysis.

Common mistakes to avoid

4 patterns
×

Setting `typeRoots` without including original @types path

Symptom
TypeScript stops finding @types packages after adding a custom typeRoots entry in tsconfig.json. Build fails with 'Cannot find module' for popular libraries like jest or node.
Fix
Always include ./node_modules/@types in the typeRoots array alongside your custom paths. For example: "typeRoots": ["./io/thecodeforge/types", "./node_modules/@types"]
×

Using `export =` but forgetting interop with ES modules

Symptom
A library that uses CommonJS module.exports gets imported with import * as in an ES module project, and the imported object is typed as { default: ... } instead of the expected shape.
Fix
For CommonJS libraries, use export = in the declaration file. For libraries that support both CJS and ESM, use a conditional exports map with separate types for each format.
×

Trusting that `declare module 'x' {}` will override a package's bundled types

Symptom
Your custom declaration file doesn't take effect — TypeScript still uses the package's own .d.ts and shows errors your custom types were supposed to fix.
Fix
Local declarations do not override bundled types from a package. You need to use module augmentation: import 'package-name' then declare module 'package-name' { ... } inside a module. Or use paths and baseUrl to redirect the package's type resolution.
×

Omitting the `types` condition in the package.json `exports` field

Symptom
Consumers of your library get any types even though you bundled .d.ts files. TypeScript falls back to an implicit any because the exports map doesn't include a types condition.
Fix
Add "types": "./dist/index.d.ts" as the first condition in every export entry. TypeScript checks the types condition before any runtime conditions.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a `.ts` file and a `.d.ts` file?
Q02SENIOR
How does TypeScript resolve the module `lodash` in a project that has `@...
Q03SENIOR
What are the trade-offs between using `export default` vs `export =` in ...
Q04SENIOR
What happens when you set `typeRoots` in tsconfig.json?
Q05SENIOR
How can you debug module resolution issues in TypeScript?
Q01 of 05JUNIOR

What is the difference between a `.ts` file and a `.d.ts` file?

ANSWER
A .ts file contains both type definitions and executable code. It can be compiled to JavaScript. A .d.ts file contains only type information — no runtime code. It is used solely by the TypeScript compiler to understand the shapes of existing JavaScript libraries or to provide type information for code that will be compiled separately.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is TypeScript Declaration Files in simple terms?
02
How do I create a declaration file for a plain JavaScript library?
03
What is the difference between `types` and `typeRoots` in tsconfig.json?
04
Why does TypeScript sometimes show `any` type for an imported module even though the module clearly exports things?
05
Can I use declaration files to augment existing types?
🔥

That's TypeScript. Mark it forged?

10 min read · try the examples if you haven't

Previous
Strict Mode in TypeScript
12 / 15 · TypeScript
Next
Mapped Types in TypeScript