Learning Guides
Menu

Type Declarations and @types

3 min readEffective TypeScript

Type Declarations and @types

Type declarations let TypeScript understand JavaScript libraries. When a library has good types, your editor becomes a superpower: autocomplete, documentation, and safe refactors.

Item 48: Prefer Official Types, Then DefinitelyTyped

Some libraries ship their own types. If not, install community types:

BASH
pnpm add -D @types/lodash

Note

Always prefer official types bundled with a library. Use @types/* when a package does not ship its own declarations.

Item 49: Know Where Types Belong (dependencies vs devDependencies)

  • Apps: types belong in devDependencies.
  • Libraries: types may belong in dependencies if your users need them at runtime.
JSON
{
  "name": "my-lib",
  "types": "dist/index.d.ts"
}

Item 50: Understand Type Resolution (types and typeRoots)

Type resolution is configured in tsconfig.json:

JSON
{
  "compilerOptions": {
    "types": ["node", "jest"],
    "typeRoots": ["./types", "./node_modules/@types"]
  }
}

Misconfiguring these can hide type packages, so use them only when you need to restrict the set of global types.

Item 51: Write Minimal Ambient Declarations First

If a package has no types, start minimal and expand as needed:

TYPESCRIPT
// types/legacy-lib.d.ts
 
declare module "legacy-lib" {
  export function parse(input: string): unknown;
}

Add more functions as you encounter them in your code.

Item 52: Prefer Module Augmentation

Augment existing libraries instead of adding globals:

TYPESCRIPT
// types/express.d.ts
import "express";
 
declare module "express" {
  interface Request {
    user?: { id: string; role: string };
  }
}

This keeps types scoped to the module.

Item 53: Use declare global Only When Needed

If you truly need globals (browser scripts, legacy APIs), use:

TYPESCRIPT
// types/global.d.ts
export {};
 
declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

Item 54: Understand Default Exports and export =

Some CommonJS libraries use module.exports = ... and need export =:

TYPESCRIPT
// types/cjs-lib.d.ts
declare function cjsLib(input: string): number;
export = cjsLib;

For ES modules, use export default:

TYPESCRIPT
declare const format: (input: string) => string;
export default format;

Item 55: Use Overloads to Model API Variants

TYPESCRIPT
declare function format(input: string): string;
declare function format(input: number): string;
declare function format(input: string | number): string;

Overloads preserve precise types for consumers.

Item 56: Add JSDoc to Improve Editor Hints

TYPESCRIPT
/**
 * Formats input into a normalized string.
 * Throws if input is empty.
 */
export function format(input: string): string;

Item 57: Keep Declarations in Sync

Type declarations must match runtime behavior. Use small tests:

TYPESCRIPT
// type-tests.ts
import { expectType } from "tsd";
expectType<string>(format("hello"));

Warning

Incorrect declarations are worse than no declarations, because they give false confidence.

Common Scenario: Wrapping an Untyped SDK

TYPESCRIPT
// types/payment-sdk.d.ts
declare module "payment-sdk" {
  export interface Charge {
    id: string;
    amount: number;
  }
 
  export function createCharge(amount: number): Promise<Charge>;
}

Key Takeaways

  1. Prefer official types, then @types/*.
  2. Keep type packages in devDependencies for apps.
  3. Start with minimal ambient declarations and expand.
  4. Prefer module augmentation to globals.
  5. Use overloads and JSDoc for precise, helpful types.

Next: writing and running TypeScript code effectively.