| |

TypeScript 67 🔷 Typing Third-Party Libraries Without Types

Most of the popular npm packages ship their own TypeScript declarations, or have an @types package on DefinitelyTyped. But a library that is new, obscure, or written by an author who does not use TypeScript often ships neither. When you import it, the compiler sees any for every export, and the type safety is lost. The good news is that you can write the types yourself — a small declaration file that describes the parts of the library you use. The bad news is that the file is a claim about the library’s behavior, and the claim can drift. This chapter covers the practical workflow: how to recognize an untyped library, how to write a minimal declaration, how to structure the declaration file, how to handle the CommonJS and ESM patterns, how to test the declaration, and when to contribute the types to DefinitelyTyped instead of keeping them local.

Key point: When a library has no types, the compiler treats every import from it as any. The fix is a local declaration file — a .d.ts file with a declare module "library-name" block that describes the library’s shape. The declaration is the developer’s claim, and the compiler trusts it. The declaration should cover the functions and the classes that the code uses, not the entire library. A minimal stub is the starting point, and the declaration can be expanded as the usage grows. When the declaration is stable and the library is widely used, the contribution to DefinitelyTyped benefits the whole community. For a library that is actively maintained, a pull request to add the types to the library itself is the better long-term fix.


Recognizing an untyped library

The first step is knowing that a library has no types. There are several signs, and the compiler’s behavior is the confirmation.

The sign 1: the import is any. The compiler accepts the import, and the properties of the imported object are any.

import { parse } from 'untyped-parser';

const result = parse('some input');
// result is any
console.log(result.field);  // no checking

The compiler does not complain about the missing declarations because the any is the fallback. The lack of the error is the sign, and the tsconfig.json‘s noImplicitAny does not catch this case because the type is the explicit any from the missing declaration.

The sign 2: the “Could not find a declaration file” error. With the noImplicitAny: true (or the strict: true), the compiler reports the missing declarations.

error TS7016: Could not find a declaration file for module 'untyped-parser'.
'/node_modules/untyped-parser/index.js' implicitly has an 'any' type.

The error’s message is the specific, and the suggestion is the @types/untyped-parser or the local declaration. The error is the sign, and the message is the instruction.

The sign 3: the package.json‘s missing fields. The library’s package.json has no types or typings field.

{
  "name": "untyped-parser",
  "version": "1.0.0",
  "main": "index.js"
}

The absence of the types field means the library does not ship its own types. The main is the runtime’s entry, and the types is the types’ entry. The absence is the sign.

The sign 4: the missing @types package. The npm view @types/untyped-parser returns the 404, which means the DefinitelyTyped does not have the package.

npm view @types/untyped-parser
# 404 Not Found - GET https://registry.npmjs.org/@types%2funtyped-parser

The 404 is the sign, and the absent @types means the library is untyped.

The sign 5: the DefinitelyTyped search. The DefinitelyTyped repository’s search returns no results, which confirms the absence.

Why the recognition matters. The recognition is the first step, and the first step determines the workflow. The missing types are the problem, and the local declaration is the fix. The recognition is the diagnosis, and the diagnosis is the skill.

Why the compiler’s error is the instruction. The TS7016 error is the specific, and the message suggests the @types package or the local declaration. The error is the instruction, and the instruction is the workflow. The error is the sign, and the sign is the start.

Why the noImplicitAny catches the case. The noImplicitAny (the strict‘s part) makes the missing declaration an error instead of the silent any. The flag is the safety, and the safety is the requirement. The modern project uses the strict, and the strict includes the noImplicitAny.


Writing a minimal declaration

The minimal declaration is the stub that covers the parts of the library that the code uses. It is the smallest file that makes the compiler stop complaining.

The basic stub. The declare module block is the module’s declaration, and the exports are the members.

// src/types/untyped-parser.d.ts
declare module 'untyped-parser' {
  export function parse(input: string): { field: string };
}

The parse function is declared with the parameter’s type and the return’s type. The compiler uses the declaration, and the call is checked.

Why the stub is the declaration. The declare module block is the ambient declaration, and the compiler treats the module as the declared. The runtime’s module is the actual, and the declaration is the claim. The two must agree, and the agreement is the discipline.

Why the stub should be minimal. The stub declares the parts that the code uses. The entire library’s API is not needed, and the minimal is the easier. The stub can be expanded as the usage grows, and the growth is the iteration.

The function’s declaration. The function’s parameters and return are the types.

declare module 'untyped-parser' {
  export interface ParseOptions {
    strict?: boolean;
    encoding?: string;
  }

  export interface ParseResult {
    field: string;
    tokens: string[];
  }

  export function parse(input: string, options?: ParseOptions): ParseResult;

  export function stringify(result: ParseResult): string;
}

The interfaces declare the shapes, and the functions declare the signatures. The types are the reusable, and the functions are the API. The declaration is the module’s, and the module is the library’s.

Why the interfaces are the reusable. The interfaces can be used in the other files, and the import is the same as the library’s. The import { ParseResult } from 'untyped-parser' works, and the type is the declared. The interfaces are the reusable, and the reusable is the efficient.

The class’s declaration. The class is declared with the constructor, the methods, and the properties.

declare module 'untyped-parser' {
  export class Parser {
    constructor(options?: ParseOptions);
    parse(input: string): ParseResult;
    stringify(result: ParseResult): string;
    static defaultOptions: ParseOptions;
  }
}

The constructor is the signature, the methods are the signatures, and the static is the property. The class is the declaration, and the declaration is the API.

Why the class is not the abstract. The declare class is not the abstract, and the class is the runtime’s. The abstract class would prevent the new, which is the wrong. The declare class is the runtime’s, and the runtime’s class is the instantiable.

The default export’s declaration. The default export is declared with the export default.

declare module 'untyped-parser' {
  const parser: {
    parse(input: string): ParseResult;
    stringify(result: ParseResult): string;
  };
  export default parser;
}

The default export is the object with the methods. The export default is the declaration, and the import is the import parser from 'untyped-parser'.

Why the default export’s shape matters. The default export is the object, the function, or the class. The shape is the declaration, and the declaration is the specific. The shape should match the runtime’s, and the match is the discipline.

Why the export = is the legacy. The CommonJS’s module.exports = ... is declared with the export = ..., and the import is the import x = require('...') or the import x from '...' (with the esModuleInterop). The export = is the legacy, and the modern is the export default. The library’s module system determines the choice, and the choice is the specific.


Structuring the declaration file

The declaration file’s location and the name are the conventions, and the conventions make the file discoverable.

The types folder. The declaration files live in the src/types or the types folder, and the tsconfig.json‘s include array includes the folder.

{
  "include": ["src", "types"]
}

The include array is the compiler’s scope, and the folder is the location. The declaration is the file, and the file is the folder’s.

Why the types folder is the convention. The types folder is the separate, and the separation is the organization. The declaration is not the source, and the folder is the distinction. The convention is the common, and the common is the discoverable.

The file’s name. The file’s name matches the library’s, which makes the mapping obvious.

types/
  untyped-parser.d.ts
  legacy-charts.d.ts
  old-analytics.d.ts

The file’s name is the library’s, and the mapping is the clear. The name is the convention, and the convention is the organization.

Why the file’s name matters. The file’s name is the search’s, and the mapping is the mental. The untyped-parser.d.ts is the library’s, and the import is the untyped-parser. The two are the same, and the same is the discoverable.

The declare module‘s name. The declare module 'untyped-parser' block’s name is the import specifier, not the file’s name.

declare module 'untyped-parser' {
  // the declarations
}

The name is the 'untyped-parser', and the import is the import { parse } from 'untyped-parser'. The two are the same, and the same is the resolution.

Why the name matters. The declare module‘s name is the import’s, and the resolution is the match. The wrong name means the declaration is not applied, and the import is the any. The name is the key, and the key is the resolution.

The scoped package’s name. The scoped package — @company/library — is declared with the full name.

declare module '@company/library' {
  // the declarations
}

The @company/library is the full name, and the import is the same. The scoped name is the string, and the string is the key.

Why the scoped name matters. The scoped package’s import is the full name, and the declaration’s name is the full name. The two are the same, and the same is the resolution. The scope is the part, and the part is the name.

The wildcard’s declaration. The wildcard declares the pattern for the file types.

declare module '*.css' {
  const content: string;
  export default content;
}

The *.css is the pattern, and the content is the string. The pattern is the asset’s, and the asset is the bundler’s. The wildcard is the common, and the common is the asset’s.

Why the wildcard matters. The asset’s imports are the bundler’s, and the TypeScript does not know them. The wildcard is the declaration, and the declaration is the type. The pattern is the extension, and the extension is the asset’s.


The CommonJS and ESM patterns

The library’s module system determines the declaration’s form. The CommonJS and the ESM are the two, and the declaration is the specific.

The CommonJS’s module.exports. The module.exports = ... is declared with the export = ....

declare module 'legacy-lib' {
  function greet(name: string): string;
  namespace greet {
    const version: string;
  }
  export = greet;
}

The export = greet is the CommonJS’s, and the import is the import greet = require('legacy-lib') or the import greet from 'legacy-lib' (with the esModuleInterop).

Why the export = matters. The export = is the CommonJS’s, and the modern’s export default is the ESM’s. The two are the different, and the different is the module system’s. The library’s module system determines the choice, and the choice is the specific.

The ESM’s export default. The export default ... is declared with the export default.

declare module 'modern-lib' {
  const api: {
    parse(input: string): ParseResult;
  };
  export default api;
}

The export default api is the ESM’s, and the import is the import api from 'modern-lib'. The two are the same, and the same is the module system’s.

Why the ESM’s export default matters. The ESM’s default export is the object, and the declaration is the shape. The shape is the specific, and the specific is the declaration. The modern is the ESM, and the ESM is the default.

The named exports. The named exports are declared with the export.

declare module 'multi-export' {
  export function parse(input: string): ParseResult;
  export function stringify(result: ParseResult): string;
  export interface ParseResult { field: string; }
}

The named exports are the three, and the import is the import { parse, stringify, ParseResult } from 'multi-export'. The named is the common, and the common is the multiple.

Why the named exports matter. The named exports are the multiple, and the import is the specific. The named is the ESM’s, and the ESM’s is the modern. The named is the common, and the common is the library’s.

The esModuleInterop‘s role. The esModuleInterop: true makes the import x from '...' work for the CommonJS’s export =, and the import * as x from '...' for the namespace. The flag is the compatibility, and the compatibility is the common.

{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

Why the esModuleInterop matters. The CommonJS’s and the ESM’s interop is the complicated, and the esModuleInterop is the compatibility. The flag is the modern, and the modern is the common. The declaration’s form is the library’s, and the import’s form is the project’s.


Testing the declaration

The declaration is the claim, and the test is the verification. The test ensures the declaration matches the runtime’s behavior, and the mismatch is the bug.

The compile-time test. The declaration is tested by the compilation, and the usage is the test.

import { parse } from 'untyped-parser';

const result = parse('input');
// result is ParseResult
// result.field is string
// result.wrongField  // ❌ error: Property 'wrongField' does not exist

The compiler’s check is the test, and the wrong property is the error. The declaration is the type, and the test is the usage. The two are the pair, and the pair is the safety.

Why the compile-time test matters. The compile-time test is the automatic, and the error is the discovery. The wrong property is the error, and the error is the fix. The compile-time test is the safety, and the safety is the value.

The runtime test. The runtime test verifies the declaration’s behavior against the library’s.

import { parse } from 'untyped-parser';

const result = parse('input');
expect(result.field).toBe('expected');
expect(typeof result.tokens).toBe('object');

The runtime test is the behavior, and the behavior is the library’s. The declaration is the type, and the runtime is the value. The two are the pair, and the pair is the verification.

Why the runtime test matters. The declaration is the claim, and the runtime is the truth. The mismatch is the bug, and the bug is the fix. The runtime test is the verification, and the verification is the value.

The mismatch’s example. The declaration says the return is the { field: string }, and the runtime returns the { value: string }. The declaration is the wrong, and the runtime is the truth. The test is the discovery, and the discovery is the fix.

// The declaration
declare module 'untyped-parser' {
  export function parse(input: string): { field: string };
}

// The runtime
const result = parse('input');
console.log(result);  // { value: 'parsed' }
// result.field is undefined

// The fix
declare module 'untyped-parser' {
  export function parse(input: string): { value: string };
}

The mismatch is the common, and the common is the discovery. The test is the mechanism, and the mechanism is the fix.

Why the test should be the part of the CI. The test is the CI’s, and the CI is the automated. The test is the check, and the check is the safety. The test is the pattern, and the pattern is the modern.

Why the test should be the simple. The test is the simple, and the simple is the effective. The compile-time test is the usage, and the runtime test is the assertion. The two are the pair, and the pair is the verification.


The declare module patterns

The declare module block has the patterns, and each has the use. The patterns are the vocabulary, and the vocabulary is the fluency.

The module’s declaration. The declare module 'name' { ... } is the module’s declaration, and the block’s contents are the module’s exports.

declare module 'my-lib' {
  export function fn(): void;
}

The fn is the module’s export, and the import is the import { fn } from 'my-lib'. The declaration is the module’s, and the module is the library’s.

The augmentation. The declare module 'existing-lib' { ... } with the import is the augmentation, and the block adds to the existing module.

import 'express';

declare module 'express' {
  interface Request {
    user?: User;
  }
}

The import 'express' makes the file a module, and the declare module block augments the express module. The block’s contents are the additions, and the additions are the merge.

Why the augmentation differs from the declaration. The declaration creates the module, and the augmentation extends the existing. The two are the same syntax, and the difference is the import. The import makes the file a module, and the module’s declare module block is the augmentation.

The global’s augmentation. The declare global block is the global’s augmentation, and the block’s contents are the global’s additions.

export {};

declare global {
  interface Window {
    myApp: App;
  }
}

The export {} makes the file a module, and the declare global block adds to the global. The additions are the global’s, and the global is the scope’s.

The wildcard’s declaration. The declare module '*.ext' { ... } is the pattern, and the pattern covers the matching modules.

declare module '*.svg' {
  const content: string;
  export default content;
}

The *.svg is the pattern, and the content is the string. The pattern is the asset’s, and the asset is the bundler’s.

Why the patterns matter. The patterns are the vocabulary, and the vocabulary is the fluency. The module’s declaration, the augmentation, the global’s, and the wildcard’s are the four, and the four cover the cases. The patterns are the skill, and the skill is the practice.


When to contribute to DefinitelyTyped

The local declaration is the temporary, and the contribution is the permanent. The choice between the two is the decision, and the decision is the value.

The local declaration’s case. The local declaration is the fast, and the fast is the value. The library is the internal, the usage is the narrow, and the contribution is the overhead. The local is the pragmatic, and the pragmatic is the choice.

The contribution’s case. The contribution is the shared, and the shared is the benefit. The library is the public, the usage is the wide, and the contribution is the value. The contribution is the community’s, and the community’s is the gift.

The library’s own types. The library that is actively maintained should have its own types, and the pull request to the library is the better fix. The DefinitelyTyped’s types are the community’s, and the library’s are the official. The library’s own is the preferred, and the preferred is the modern.

Why the library’s own is the preferred. The library’s own types are maintained by the library’s authors, and the authors know the API. The DefinitelyTyped’s are maintained by the volunteers, and the volunteers are the community. The library’s own is the authoritative, and the authoritative is the preferred.

The contribution’s workflow. The contribution is the pull request to the DefinitelyTyped repository, and the process is the documented. The contribution is the effort, and the effort is the community’s.

Why the contribution matters. The contribution is the shared, and the shared is the benefit. The contributor’s effort is the community’s gift, and the gift is the value. The contribution is the pattern, and the pattern is the modern.

The library’s .d.ts‘s option. The library can include the .d.ts file in the package, and the types field declares the entry. The library’s own is the modern, and the modern is the preferred.

Why the library’s .d.ts is the best. The library’s .d.ts is the versioned with the library, and the versioning is the sync. The DefinitelyTyped’s is the separate, and the separate is the drift. The library’s own is the sync, and the sync is the best.


Complete Example Session

// ============================================
// PART 1: THE UNTYPED IMPORT
// ============================================

import { parse } from 'untyped-parser';

const result = parse('some input');
// result is any
// No checking.

// ============================================
// PART 2: THE COMPILER ERROR
// ============================================

// With noImplicitAny:
// error TS7016: Could not find a declaration file for module 'untyped-parser'.

// ============================================
// PART 3: THE MINIMAL STUB
// ============================================

// types/untyped-parser.d.ts
declare module 'untyped-parser' {
  export function parse(input: string): { field: string };
}

// The import is now checked.

// ============================================
// PART 4: THE FULLER DECLARATION
// ============================================

// types/untyped-parser.d.ts
declare module 'untyped-parser' {
  export interface ParseOptions {
    strict?: boolean;
    encoding?: string;
  }

  export interface ParseResult {
    field: string;
    tokens: string[];
  }

  export function parse(input: string, options?: ParseOptions): ParseResult;

  export function stringify(result: ParseResult): string;

  export class Parser {
    constructor(options?: ParseOptions);
    parse(input: string): ParseResult;
    stringify(result: ParseResult): string;
    static defaultOptions: ParseOptions;
  }
}

// ============================================
// PART 5: THE USAGE
// ============================================

import { parse, Parser, ParseResult } from 'untyped-parser';

const result: ParseResult = parse('input', { strict: true });
// result.field is string
// result.tokens is string[]

const parser = new Parser();
const result2 = parser.parse('input');

// ============================================
// PART 6: THE COMMONJS'S EXPORT =
// ============================================

// types/legacy-lib.d.ts
declare module 'legacy-lib' {
  function greet(name: string): string;
  namespace greet {
    const version: string;
  }
  export = greet;
}

// The import:
import greet = require('legacy-lib');
// or with esModuleInterop:
import greet from 'legacy-lib';

// ============================================
// PART 7: THE SCOPED PACKAGE
// ============================================

// types/company-library.d.ts
declare module '@company/library' {
  export function initialize(config: Config): void;
  export interface Config {
    apiKey: string;
    endpoint: string;
  }
}

// The import:
import { initialize } from '@company/library';

// ============================================
// PART 8: THE TEST
// ============================================

// types/untyped-parser.d.ts
declare module 'untyped-parser' {
  export function parse(input: string): { field: string };
}

// The test:
import { parse } from 'untyped-parser';

const result = parse('input');
console.log(result.field);  // ✅
// console.log(result.wrong);  // ❌ error

// The runtime:
// The declaration says { field: string }
// The runtime returns { field: 'parsed' }
// The two agree.

// ============================================
// PART 9: THE MISMATCH
// ============================================

// The declaration says the return is { field: string }
// But the runtime returns { value: string }

// The test catches the mismatch:
const result = parse('input');
// result.field is undefined at runtime

// The fix:
declare module 'untyped-parser' {
  export function parse(input: string): { value: string };
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't cast to any
const parser = require('untyped-parser') as any;  // loses checking

// Don't declare the whole library
// The minimal stub is enough.

// Don't forget the export = for CommonJS
// The export default is for ESM.

// Don't put the stub in the source folder
// Use the types/ folder.

// Don't forget the include
// The tsconfig's include must have the folder.

// Don't rely on the declaration without the test
// The runtime test is the verification.

The ten parts cover the untyped import, the compiler error, the minimal stub, the fuller declaration, the usage, the CommonJS’s export =, the scoped package, the test, the mismatch, and the anti-patterns.


Quick Reference

The Signs of an Untyped Library

SignConfirmation
The import is anyThe compiler’s silence
The TS7016 errorThe noImplicitAny
The missing types fieldThe package.json
The missing @typesThe npm view 404
The DefinitelyTyped’s searchThe repository

The Declaration’s Form

The library’s moduleThe declaration
The ESM’s namedexport function ...
The ESM’s defaultexport default ...
The CommonJS’s module.exportsexport = ...
The namespaceexport namespace ...

The File’s Location

The locationThe purpose
types/*.d.tsThe declarations
src/types/*.d.tsThe alternative
The tsconfig‘s includeThe scope

The Patterns

The patternThe use
declare module 'name' {}The module’s declaration
import 'name'; declare module 'name' {}The augmentation
declare global {}The global’s
declare module '*.ext' {}The wildcard

The Test’s Types

The testThe purpose
The compile-timeThe type’s check
The runtimeThe behavior’s check
The CIThe automated

The Contribution

The caseThe choice
The internal libraryThe local declaration
The public libraryThe DefinitelyTyped
The maintained libraryThe library’s own types

Best Practices

✅ Do This:

// Write the minimal stub
declare module 'untyped-parser' {
  export function parse(input: string): ParseResult;
}                                                              // ✅

// Use the types/ folder
// types/untyped-parser.d.ts                                   // ✅

// Include the folder
{ "include": ["src", "types"] }                                // ✅

// Use the export = for CommonJS
declare module 'legacy-lib' {
  function greet(name: string): string;
  export = greet;
}                                                              // ✅

// Test the declaration
import { parse } from 'untyped-parser';
const result = parse('input');
console.log(result.field);                                     // ✅

// Contribute to DefinitelyTyped
// The public library's types.                                 // ✅

❌ Don’t Do This:

// Don't cast to any
const parser = require('untyped-parser') as any;               // ⚠️

// Don't declare the whole library
// The minimal stub is enough.                                 // ⚠️

// Don't forget the export = for CommonJS
declare module 'legacy-lib' {
  export default greet;  // the wrong for module.exports      // ⚠️
}

// Don't put the stub in the source folder
// Use the types/ folder.                                      // ⚠️

// Don't forget the include
{ "include": ["src"] }  // the types/ is not included          // ⚠️

// Don't rely on the declaration without the test
// The runtime test is the verification.                       // ⚠️

Common Pitfalls

PitfallProblemSolution
The cast to anyThe lost checkingWrite the stub
The export default for the CJSThe wrong moduleUse export =
The stub not includedThe declaration ignoredAdd the include
The wrong module’s nameThe resolution failsMatch the import
The mismatch with the runtimeThe wrong typeTest the declaration
The whole library’s declarationThe maintenanceThe minimal
The any‘s defaultsThe uncheckedThe specific

Real-World Examples

1. The minimal stub

declare module 'untyped-parser' {
  export function parse(input: string): ParseResult;
}

2. The interfaces

declare module 'untyped-parser' {
  export interface ParseResult { field: string; }
}

3. The class

declare module 'untyped-parser' {
  export class Parser {
    constructor(options?: ParseOptions);
    parse(input: string): ParseResult;
  }
}

4. The CommonJS

declare module 'legacy-lib' {
  function greet(name: string): string;
  export = greet;
}

5. The scoped package

declare module '@company/library' {
  export function initialize(config: Config): void;
}

6. The wildcard

declare module '*.css' {
  const content: string;
  export default content;
}

7. The types folder

types/
  untyped-parser.d.ts
  legacy-lib.d.ts

8. The include

{ "include": ["src", "types"] }

9. The test

import { parse } from 'untyped-parser';
const result = parse('input');
console.log(result.field);

10. The augmentation

import 'express';
declare module 'express' {
  interface Request { user?: User; }
}

Visual: The Untyped Library

┌──────────────────────────────────────────────────────────┐
│  THE LIBRARY'S PACKAGE.JSON                              │
│    {                                                     │
│      "name": "untyped-parser",                           │
│      "version": "1.0.0",                                 │
│      "main": "index.js"                                  │
│    }                                                     │
│                                                          │
│  NO "types" field → the library ships no types.          │
│                                                          │
│  THE IMPORT                                              │
│    import { parse } from 'untyped-parser';               │
│    → parse is any                                        │
│                                                          │
│  THE COMPILER'S ERROR (with noImplicitAny)               │
│    error TS7016: Could not find a declaration file.      │
│                                                          │
│  THE FIX                                                 │
│    The local declaration file.                           │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Declaration

┌──────────────────────────────────────────────────────────┐
│  types/untyped-parser.d.ts                               │
│                                                          │
│  declare module 'untyped-parser' {                       │
│    export interface ParseOptions { ... }                 │
│    export interface ParseResult { ... }                  │
│    export function parse(input: string, options?: ParseOptions): ParseResult;│
│    export function stringify(result: ParseResult): string;│
│    export class Parser { ... }                           │
│  }                                                       │
│                                                          │
│  The module's declaration.                               │
│  The compiler uses it.                                   │
│  The runtime does not.                                   │
│                                                          │
│  THE IMPORT                                              │
│    import { parse, Parser, ParseResult } from 'untyped-parser';│
│    → The types are the declared.                         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Module Systems

┌──────────────────────────────────────────────────────────┐
│  THE ESM                                                 │
│    The library: export function parse() {}               │
│    The declaration: export function parse(): ...;        │
│    The import: import { parse } from 'lib';              │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE COMMONJS                                            │
│    The library: module.exports = greet;                  │
│    The declaration: export = greet;                      │
│    The import: import greet = require('lib');            │
│    Or (with esModuleInterop): import greet from 'lib';   │
│                                                          │
│  The module system determines the declaration's form.    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Test

┌──────────────────────────────────────────────────────────┐
│  THE DECLARATION                                         │
│    declare module 'untyped-parser' {                     │
│      export function parse(input: string): { field: string };│
│    }                                                     │
│                                                          │
│  THE COMPILE-TIME TEST                                   │
│    const result = parse('input');                        │
│    console.log(result.field);   // ✅                    │
│    console.log(result.wrong);   // ❌ the error          │
│                                                          │
│  THE RUNTIME TEST                                        │
│    const result = parse('input');                        │
│    console.log(result);  // { field: 'parsed' }          │
│    The declaration and the runtime agree.                │
│                                                          │
│  THE MISMATCH                                            │
│    The declaration says { field: string }                │
│    The runtime returns { value: string }                 │
│    result.field is undefined                             │
│    The fix: the declaration's correction.                │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Contribution

┌──────────────────────────────────────────────────────────┐
│  THE LOCAL DECLARATION                                   │
│    types/untyped-parser.d.ts                             │
│    The fast, the pragmatic.                              │
│    The internal library's use.                           │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE DEFINITELY TYPED                                    │
│    A pull request to the repository.                     │
│    The shared, the community's.                          │
│    The public library's use.                             │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE LIBRARY'S OWN TYPES                                 │
│    The .d.ts in the package.                             │
│    The library's authors' maintenance.                   │
│    The preferred, the modern.                            │
│                                                          │
│  The library's own is the best.                          │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
The untyped libraryNo types
The signThe TS7016 or the any
The fixThe local declaration
The locationThe types/*.d.ts
The declarationThe declare module 'name' {}
The CommonJSThe export = ...
The ESMThe export default ...
The testThe compile-time and the runtime
The contributionThe DefinitelyTyped or the library
The preferredThe library’s own types

Key takeaways:

  • An untyped library is one that ships no types — the compiler sees any for every import, and the TS7016 error (with noImplicitAny) is the sign
  • The fix is a local declaration file — the declare module 'library-name' {} block describes the library’s shape, and the compiler trusts the claim
  • The declaration should be minimal — it covers the parts of the library that the code uses, and the whole library is not needed
  • The declaration’s form depends on the library’s module system — the ESM’s export default and the named exports, the CommonJS’s export = ...
  • The types/ folder is the convention — the declaration files live in the folder, and the tsconfig.json‘s include array includes it
  • The declaration must be tested — the compile-time test verifies the types, and the runtime test verifies the behavior, and the mismatch is the bug
  • The declare module‘s name is the import’s — the two must match, and the match is the resolution
  • The scoped package’s name is the full name — the @company/library is the declaration’s name, and the import is the same
  • The wildcard declares the asset’s pattern — the declare module '*.css' {} is the pattern, and the asset is the bundler’s
  • The contribution is the choice — the local declaration is the fast and the pragmatic, the DefinitelyTyped is the shared, and the library’s own types are the preferred

Remember: A third-party library without types is not the end. The local declaration is the fix, and the declare module block is the mechanism. The declaration is the claim, the test is the verification, and the contribution is the gift. The ESM’s export default and the CommonJS’s export = are the forms, and the module system determines the choice. The types/ folder is the location, the include is the scope, and the minimal stub is the start. The types are the safety, and the safety is the value.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!