Type Untyped Packages in TypeScript

Learn how to add TypeScript types to npm packages that ship without them. Create declaration files for untyped JavaScript libraries so you get autocomplete and type safety.

7 min read

To type untyped packages in TypeScript, you write your own declaration file, since the compiler has no type information for a package written in plain JavaScript. Without one, installing that package gives you a compiler error and no autocomplete or type checking.

The fix is to create your own declaration file. This article shows you how to add TypeScript types to any JavaScript package, from a quick one-line shim to a full type definition.

Step 1: Check If Types Already Exist

Before writing anything, check whether types are already available:

  • Run npm install --save-dev @types/package-name. Many popular packages have community-maintained types on DefinitelyTyped.
  • Look inside the package's node_modules folder for declaration files. Some packages bundle their own types.
  • Check the package's package.json for a types or typings field.

If the package has no community types, npm rejects the install with a 404 instead of adding a dependency. That error is your signal to write the declaration file yourself.

texttext
npm error 404 '@types/package-name@*' is not in this registry.

This error means no community types exist. Time to create them.

Step 2: The Quick Shim

The fastest way to silence the compiler error is a minimal shim. Create a declaration file that tells TypeScript the module exists, but treat everything as any:

typescripttypescript
// types/untyped-pkg.d.ts
declare module "untyped-pkg";

With this one line, TypeScript stops complaining because the module now exists as far as the compiler is concerned. You can import the package normally, though every value coming from it is typed as any.

typescripttypescript
import something from "untyped-pkg";
// something is typed as any

This shim is a temporary placeholder, not a permanent solution. You get no type checking, but at least the project compiles. Replace it with real types as soon as you understand the API you are using.

Step 3: Set Up the Declaration File Folder

Create a dedicated folder for your custom type declarations:

texttext
your-project/
  src/
  types/
    untyped-pkg.d.ts
    another-pkg.d.ts
  tsconfig.json

The types folder sits next to src, at the project root, and holds one declaration file per untyped package. Then tell TypeScript about the folder in your tsconfig.json.

jsonjson
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": ["src", "types"]
}

The typeRoots option tells TypeScript where to look for type packages. The include option ensures your declaration files are part of the compilation. If you do not want to touch typeRoots, simply make sure the folder is under an already-included directory like src.

Step 4: Add Real Types for the Parts You Use

Start with the smallest useful declaration. You do not need to type the entire package. Type only the exports you actually import.

Here is an imaginary untyped package called color-utils with this API:

javascriptjavascript
// color-utils (JavaScript you cannot edit)
function hexToRgb(hex) {
  // Parses "#ff0000" into { r: 255, g: 0, b: 0 }
}
 
function rgbToHex(r, g, b) {
  // Converts (255, 0, 0) to "#ff0000"
}
 
module.exports = { hexToRgb, rgbToHex };

Write a declaration file that covers just the two functions you use, plus a small RGB interface to describe the shape that hexToRgb returns.

typescripttypescript
// types/color-utils.d.ts
declare module "color-utils" {
  interface RGB {
    r: number;
    g: number;
    b: number;
  }
 
  export function hexToRgb(hex: string): RGB;
  export function rgbToHex(r: number, g: number, b: number): string;
}

With that declaration in place, your TypeScript code gets full type checking on both functions, including autocomplete on the returned RGB object.

typescripttypescript
import { hexToRgb, rgbToHex } from "color-utils";
 
const color = hexToRgb("#ff0000");
console.log(color.r); // 255 (TypeScript knows this is a number)
 
const hex = rgbToHex(255, 0, 0);
console.log(hex); // "#ff0000"

Step 5: Handle Different Export Styles

Packages export in different ways depending on their module format. Match your declaration to the export style, since a mismatched shape produces a declaration that never lines up with how consumers actually import the package.

CommonJS with Default Export

Some packages assign a single function directly to module.exports, so importing the package gives you that function and nothing else.

javascriptjavascript
// package exports a single function
module.exports = function parseQuery(url) {
  return Object.fromEntries(new URLSearchParams(url));
};

Match that shape with export =, which tells TypeScript the entire module is this one callable value rather than an object of named exports.

typescripttypescript
declare module "query-parser" {
  function parseQuery(url: string): Record<string, string>;
  export = parseQuery;
}

CommonJS with Named Exports

Other packages export a plain object with multiple properties, closer to how a typical ES module looks.

javascriptjavascript
// package exports an object with multiple properties
module.exports = { encode, decode };

For this shape, use regular named export function declarations inside the declare module block, one for each property on the exported object, the same way you would declare exports for a normal ES module.

typescripttypescript
declare module "codec-lib" {
  export function encode(input: string): Buffer;
  export function decode(input: Buffer): string;
}

Default Export with Additional Named Exports

A third common shape combines both: a callable default export with extra properties attached to it, a pattern common in older utility libraries.

javascriptjavascript
// package has a default export plus named helpers
module.exports = mainFunction;
module.exports.helper = helperFunction;

Declare the main function and the helper as siblings inside the block, then use export = on the main function. TypeScript automatically treats the helper as a property attached to it.

typescripttypescript
declare module "mixed-pkg" {
  interface Config { verbose?: boolean; }
  interface Result { success: boolean; }
 
  function mainFunction(config: Config): Result;
  function helperFunction(input: string): boolean;
 
  export = mainFunction;
  // helperFunction is inferred as a property on mainFunction.
}

For more on the full range of export patterns, see declaration files in TypeScript.

Step 6: Type Classes and Constructors

If the package exports a class constructor, declare a class inside the module block and list its methods without bodies, the same way you would inside a normal declaration file.

typescripttypescript
declare module "event-emitter" {
  class EventEmitter {
    on(event: string, handler: (...args: unknown[]) => void): void;
    emit(event: string, ...args: unknown[]): void;
    removeListener(event: string, handler: (...args: unknown[]) => void): void;
  }
 
  export = EventEmitter;
}

If the class also has static methods, like a factory method for creating connections, declare them inside the class body using the static keyword, exactly as you would in a runnable TypeScript class.

typescripttypescript
declare module "db-client" {
  class Database {
    static connect(url: string): Database;
    query<T>(sql: string): Promise<T[]>;
    close(): Promise<void>;
  }
 
  export = Database;
}

Step 7: Type Callback-Heavy APIs

Many older JavaScript packages use callbacks instead of promises. Type these with a function signature whose last parameter is the callback, matching the error-first convention common in Node.js APIs.

typescripttypescript
declare module "legacy-fs" {
  export function readFile(
    path: string,
    callback: (error: Error | null, data: string) => void
  ): void;
 
  export function writeFile(
    path: string,
    data: string,
    callback: (error: Error | null) => void
  ): void;
}

For packages with config objects, use an interface to describe the options bag instead of inlining every property directly in the function signature. The example below adds an options parameter to a second overload of readFile.

typescripttypescript
declare module "legacy-fs" {
  interface ReadOptions {
    encoding?: string;
    flag?: string;
  }
 
  export function readFile(
    path: string,
    options: ReadOptions,
    callback: (error: Error | null, data: string) => void
  ): void;
}

Build the types incrementally. Each time you use a new method from the package, add its signature to the declaration file.

When to Stop Typing

You do not need to type every function, every overload, and every edge case. Type what you use, and leave the rest for later. A partial declaration file is better than no declaration file.

If the package is small and well-documented, typing it completely may be worth the time. If it is large and you only use two functions, type those two and move on.

A pragmatic checklist:

SituationAction
You use 2-3 functions from a large packageType only those functions
Package has good documentationType more detail since the contract is clear
Package is internal/private to your teamFull typing is a good investment
Package changes frequentlyKeep declarations minimal to reduce maintenance
You will contribute types to DefinitelyTypedType the full public API

Testing Your Types

The best way to verify your declarations is to use them. Write a small TypeScript file that imports the package and exercises the functions you typed:

typescripttypescript
// test-types.ts
import { hexToRgb } from "color-utils";
 
// Compiler should accept this:
const color = hexToRgb("#ff0000");
 
// Compiler should reject this:
const bad = hexToRgb(123);
//                   ^ Argument of type 'number' is not assignable to parameter of type 'string'.

If the compiler accepts the correct call and rejects the incorrect one, your types are working. You do not need to run this test file -- just checking that it compiles is enough.

Organizing as You Scale

When you have types for several packages, keep them organized with one declaration file per package rather than combining them.

texttext
types/
  untyped-pkg.d.ts
  color-utils.d.ts
  legacy-fs.d.ts
  index.d.ts

Avoid putting declarations for multiple packages in one file. It becomes hard to tell which declaration belongs to which package and makes it difficult to remove a type when you stop using a dependency.

For a more detailed pattern on ambient declarations, see ambient declarations in TypeScript.

Rune AI

Rune AI

Key Insights

  • Use declare module 'pkg' { ... } to add types for an untyped npm package.
  • Start with a minimal shim (declare module 'pkg';) to silence errors, then add real types incrementally.
  • Put custom .d.ts files in a dedicated folder and reference them in tsconfig.json.
  • Type only the parts of the API you actually use -- you can always add more later.
  • Check DefinitelyTyped and the package's own repository for @types before writing your own.
RunePowered by Rune AI

Frequently Asked Questions

What do I do when a package has no @types and no bundled types?

Create your own .d.ts file with a declare module block for the package. Start with the parts of the API you actually use, then expand as needed. You can also create a quick shim with declare module 'pkg'; to treat it as any temporarily.

Where should I put my custom declaration files?

Create a types/ or @types/ folder at your project root and add a .d.ts file for each untyped package. Make sure the folder is included in your tsconfig.json include or typeRoots. Many projects use src/types/ as a convention.

Can I contribute my types back to DefinitelyTyped?

Yes. If you write complete types for a public package, consider submitting them to DefinitelyTyped so other developers can benefit. The contribution process is documented on the DefinitelyTyped GitHub repository.

Conclusion

Typing untyped packages is a practical skill every TypeScript developer needs. You do not have to type every method the package exports. Start with what you use, add more as needed, and organize your declaration files so they are easy to find and maintain.