TypeScript 62 🔷 Using @types and DefinitelyTyped
A JavaScript library written in plain JavaScript ships no types. When a TypeScript consumer imports it, the compiler sees any for every export, and the type safety is lost. The solution is a type declaration file — a .d.ts file that describes the library’s shape — and the ecosystem’s mechanism for distributing those files is the @types organization on npm. The @types packages are maintained by the DefinitelyTyped project, a community effort that hosts type definitions for thousands of JavaScript libraries. This chapter covers what the @types packages are, how the compiler finds them, how to install them, how to write your own when none exists, how to contribute to DefinitelyTyped, and the configuration options that determine whether the types are used.
Key point: A JavaScript library that ships no types can have its types published as an @types/<library-name> package on npm. The DefinitelyTyped repository is the source, and the packages are published automatically from it . TypeScript finds the @types packages in node_modules/@types automatically, with no import or configuration needed . The types and typeRoots compiler options control which packages are loaded. When no @types package exists, a declare module stub provides a scoped type for the parts of the library you use . Contributing to DefinitelyTyped means writing the .d.ts file, testing it, and opening a pull request.
What the @types packages are
The @types organization on npm is a scope that hosts type declaration packages. Each package is named @types/<library>, and it contains the .d.ts files for the library. The @types/express package contains the types for Express, the @types/lodash package contains the types for Lodash, and so on .
Why the @types scope exists. A library that is written in JavaScript and does not ship its own types needs someone to write them. The library’s authors may not want to maintain the types, or the library may predate TypeScript. The DefinitelyTyped project fills the gap, and the @types scope is where the results are published .
Why the packages are separate. The @types packages are versioned separately from the libraries they describe. A library can release a new version, and the types can be updated independently. The separation allows the types to be maintained by the community without waiting for the library’s authors .
Why the compiler finds them automatically. TypeScript looks in node_modules/@types by default. When a package is imported, the compiler checks the @types directory for a matching package. If the package is present, the types are used. No import or configuration is needed .
Why the automatic discovery matters. The mechanism is what makes the types transparent to the consumer. The consumer installs the library and the @types package, and the compiler uses the types. The import in the code is the same as it would be for a TypeScript library .
Why the @types packages are not always available. The DefinitelyTyped project covers thousands of libraries, but not every library has a types package. A library that is obscure, newly published, or not widely used may have no types. In that case, the consumer writes their own stub or contributes to DefinitelyTyped .
Why the types can be outdated. The @types packages are maintained by volunteers, and they can lag behind the library’s releases. A library that changes its API and does not update the types leaves the consumers with the wrong types. The issue is reported on the DefinitelyTyped repository, and the fix is a pull request .
Why the @types packages are a community effort. The DefinitelyTyped repository is one of the largest TypeScript projects, with thousands of contributors and thousands of packages. The project is not run by the library authors; it is run by the community. The quality varies, and the maintenance depends on the volunteers .
How the compiler finds the types
TypeScript resolves the types through a specific set of rules. The @types directory is the default location, and the compiler’s behavior is controlled by the types and typeRoots options.
The default location. The compiler looks in node_modules/@types by default. Each subdirectory is treated as a package, and the index.d.ts or the types field in the package.json is the entry point .
The typeRoots option. The typeRoots option changes the directories that are searched for the type packages. The default is ["node_modules/@types"], and a custom value replaces it .
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./typings"]
}
}
Why the typeRoots is not a general-purpose loader. The typeRoots directories must contain packages in the npm format — a folder with a package.json or an index.d.ts. A loose .d.ts file in a directory is not loaded by the typeRoots mechanism. The loose files are included through the include or files options .
The types option. The types option limits which packages are loaded from the typeRoots. By default, every package in the typeRoots is loaded. The types array specifies a subset .
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
Why the types option is useful. The option restricts the global type packages to the ones the project uses. A project that does not use Jest does not need the Jest types, and the types array excludes them. The restriction is the performance and the clarity .
Why the types array is not the same as the import. The types array controls the global type packages. A package that is imported in the code is resolved through the normal module resolution, and the types array does not affect it. The two are separate mechanisms .
Why the skipLibCheck option matters. The skipLibCheck: true option tells the compiler to skip the type checking of the declaration files. The option speeds up the compilation significantly — one measurement showed a 29% improvement — because the compiler does not check the internals of the @types packages .
{
"compilerOptions": {
"skipLibCheck": true
}
}
Why the skipLibCheck is recommended. The declaration files are supposed to be correct, and the errors within them are the library’s responsibility, not the consumer’s. The skipLibCheck skips the internal checking, and the consumer’s code is still checked against the types. The option is the modern default .
Installing the @types packages
The @types packages are installed with the package manager, the same as any other dependency.
npm install --save-dev @types/express
The --save-dev flag is the convention because the types are a development-time dependency. The types are not needed at runtime, and the production install does not need them .
Why the devDependencies is the convention. The types are used by the compiler, not by the runtime. The production deployment does not need the .d.ts files. The devDependencies section is the right place, and the production install with --production skips them .
Why the dependency types are also needed. A library’s types may depend on the types of another library. The @types/express package depends on the @types/node package, and the npm install pulls the dependency. The chain is automatic, and the consumer does not have to install each one .
Why the types are installed in node_modules/@types. The npm install places the @types packages in the node_modules/@types directory. The compiler finds them there, and the import in the code does not need to change .
Why the scoped packages work. A scoped library — @company/library — has a scoped types package — @types/company__library — and the npm handles the naming. The install is the same, and the compiler finds the types .
Why the types can be bundled instead. A TypeScript library can bundle its own types, and the @types package is not needed. The library’s package.json has the types field, and the compiler uses the bundled declarations. The bundling is the preferred approach for a library that is written in TypeScript .
Why the choice matters. A JavaScript library that does not bundle its types needs the @types package. A TypeScript library bundles its types and does not need the @types package. The consumer installs the library, and the types come with it or from the @types package .
Writing a stub for an untyped library
When no @types package exists, the consumer writes a stub declaration. The stub is a .d.ts file with a declare module block that describes the parts of the library that are used.
// types/legacy-charts.d.ts
declare module 'legacy-charts' {
export interface ChartSeries {
name: string;
data: number[];
}
export function render(el: HTMLElement, series: ChartSeries[]): void;
}
The declare module 'legacy-charts' block declares the module’s shape, and the export on each declaration makes it part of the module’s interface. The consumer’s import is checked against the stub .
Why the stub is better than any. The const charts = require('legacy-charts') as any disables the type checking for everything the library exports. The stub scopes the gap to the functions that are used, and the rest of the code is still checked. The stub is the modern pattern .
Why the stub should be minimal. The stub declares the parts of the library that are used. It does not need to be complete. A minimal stub is easier to write, and it can be expanded as the usage grows .
Why the stub should be in a types folder. The convention is to put the stubs in a types directory, and the tsconfig.json‘s include array includes the folder. The organization makes the stubs discoverable, and the include makes them part of the compilation .
{
"include": ["src", "types"]
}
Why the stub should be named after the library. The file name should match the library’s name, which makes the mapping obvious. The legacy-charts.d.ts is the stub for the legacy-charts library .
Why the stub is a temporary measure. The stub is the local fix. The proper fix is to contribute the types to DefinitelyTyped, which publishes the @types package for everyone. The stub is the bridge until the package exists .
Why the stub can be replaced by the @types package. When the @types package is published, the consumer installs it, and the stub is removed. The two cannot coexist for the same module, and the @types package is the preferred source .
Contributing to DefinitelyTyped
The DefinitelyTyped repository is the source of the @types packages. Contributing means writing the .d.ts file, testing it, and opening a pull request.
The repository structure. The repository has a types directory, and each library has a subdirectory with the library’s name. The subdirectory contains the index.d.ts, the tsconfig.json, and the test files. The naming convention is the library’s name, with the scope’s @ and / replaced by __ .
The index.d.ts. The index.d.ts is the declaration file. It describes the library’s shape, and it is the file that is published as the @types package.
The tsconfig.json. The tsconfig.json in the library’s directory configures the compilation of the declaration file. It sets the lib, the strict, and the other options that the declaration needs .
The test files. The test files are TypeScript files that use the library’s types. The tests are compiled against the declaration file, and the compilation catches the errors. The tests are the verification .
The package.json. The package.json declares the dependencies and the metadata. The @types packages are published from the repository, and the package.json is the source .
The pull request. The contribution is a pull request to the repository. The PR is reviewed by the maintainers and the library’s authors, and the feedback is addressed. When the PR is merged, the @types package is published automatically .
The types-publisher tool. The types-publisher tool publishes the packages from the repository to the @types scope. The tool is run by the maintainers, and the packages are published on a schedule. The consumer installs the package, and the types are available .
Why the contribution matters. The @types packages are a community resource. A library that has no types is a library that is harder to use in TypeScript. A contribution makes the library usable, and the effort is shared across all the consumers .
Why the contribution can be declined. The library’s authors may prefer to bundle their own types, and the @types package is not needed. The DefinitelyTyped maintainers coordinate with the library’s authors, and the contribution is redirected to the library if the authors want to maintain the types themselves .
Why the contribution is a commitment. The
@typespackages are maintained by volunteers, and a contribution means agreeing to maintain the types. The types must be updated when the library changes, and the maintenance is the ongoing work. The contribution is not a one-time effort .
Complete Example Session
// ============================================
// PART 1: THE UNTYPED LIBRARY
// ============================================
// A JavaScript library with no types.
// When imported, the compiler sees `any`.
import { render } from 'legacy-charts';
// render is `any`
render(el, series); // no checking
// ============================================
// PART 2: THE STUB DECLARATION
// ============================================
// types/legacy-charts.d.ts
declare module 'legacy-charts' {
export interface ChartSeries {
name: string;
data: number[];
}
export function render(el: HTMLElement, series: ChartSeries[]): void;
}
// The import is now checked against the stub.
// ============================================
// PART 3: THE TSCONFIG
// ============================================
// tsconfig.json
// {
// "compilerOptions": {
// "strict": true,
// "skipLibCheck": true
// },
// "include": ["src", "types"]
// }
// ============================================
// PART 4: THE CHECKED IMPORT
// ============================================
import { render } from 'legacy-charts';
render(el, [{ name: 'sales', data: [1, 2, 3] }]); // ✅ checked
// render(el, 'wrong'); // ❌ error: the argument is not a ChartSeries[]
// ============================================
// PART 5: THE @TYPES PACKAGE
// ============================================
// npm install --save-dev @types/express
import express from 'express';
// The types are from node_modules/@types/express
// ============================================
// PART 6: THE TYPE OPTION
// ============================================
// tsconfig.json
// {
// "compilerOptions": {
// "types": ["node", "jest"]
// }
// }
// Only the node and jest types are loaded globally.
// The other @types packages are not.
// ============================================
// PART 7: THE SKIPLIBCHECK OPTION
// ============================================
// tsconfig.json
// {
// "compilerOptions": {
// "skipLibCheck": true
// }
// }
// The declaration files are not internally checked.
// The compilation is faster.
// The consumer's code is still checked against the types.
// ============================================
// PART 8: THE CONTRIBUTION
// ============================================
// The DefinitelyTyped repository
// types/legacy-charts/
// index.d.ts
// tsconfig.json
// legacy-charts-tests.ts
// package.json
// The pull request is reviewed and merged.
// The @types/legacy-charts package is published.
// ============================================
// PART 9: THE MIGRATION
// ============================================
// Step 1: Install the @types/legacy-charts package.
// Step 2: Remove the local stub.
// Step 3: Verify the imports.
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't cast the import to any
// const charts = require('legacy-charts') as any; // loses checking
// Don't forget the export {} in a global augmentation
// The declare global block requires the module.
// Don't put the stub in the same folder as the source
// Use a separate types/ folder.
// Don't skip the tests in the contribution
// The tests are the verification.
// Don't assume the @types package is always up to date
// The community maintains it, and it can lag.
The ten parts cover the untyped library, the stub, the tsconfig, the checked import, the @types package, the types option, the skipLibCheck, the contribution, the migration, and the anti-patterns.
Quick Reference
The @types Mechanism
| Item | Value |
|---|---|
| Location | node_modules/@types |
| Naming | @types/<library> |
| Source | DefinitelyTyped repository |
| Discovery | Automatic |
| Install | npm install --save-dev @types/<library> |
The Compiler Options
| Option | Purpose |
|---|---|
typeRoots | The directories for the type packages |
types | The specific packages to load |
skipLibCheck | Skip the internal declaration checking |
The Stub Declaration
| Form | Purpose |
|---|---|
declare module 'name' {} | Declare a module’s shape |
declare global {} | Augment the global scope |
export {} | Make the file a module |
The Contribution
| Item | Purpose |
|---|---|
index.d.ts | The declaration file |
tsconfig.json | The compilation config |
*-tests.ts | The tests |
package.json | The metadata |
| Pull request | The submission |
Best Practices
✅ Do This:
// Install the @types package
npm install --save-dev @types/express // ✅
// Write a stub for an untyped library
declare module 'legacy-charts' {
export function render(el: HTMLElement): void;
} // ✅
// Include the types folder
{ "include": ["src", "types"] } // ✅
// Use skipLibCheck for the performance
{ "compilerOptions": { "skipLibCheck": true } } // ✅
// Limit the global types
{ "compilerOptions": { "types": ["node", "jest"] } } // ✅
// Contribute to DefinitelyTyped
// types/legacy-charts/index.d.ts
// types/legacy-charts/legacy-charts-tests.ts
// Pull request // ✅
❌ Don’t Do This:
// Don't cast the import to any
const charts = require('legacy-charts') as any; // ⚠️
// Don't forget the export {} in a global augmentation
declare global { interface Window { myApp: App; } } // ❌ // ⚠️
// Don't put the stub in the source folder
// Use a types/ folder. // ⚠️
// Don't forget the include
{ "include": ["src"] } // the types/ folder is not included // ⚠️
// Don't skip the tests in the contribution
// The tests are the verification. // ⚠️
// Don't assume the @types package is always correct
// The community maintains it, and it can lag. // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
No @types package | The import is any | Write a stub |
| Stub not included | The types are ignored | Add the include |
export {} missing | The augmentation is ignored | Add the export {} |
skipLibCheck off | Slow compilation | Set it to true |
types array too broad | Unnecessary loading | Limit the packages |
| Contribution without tests | The PR is rejected | Add the tests |
| Stub in the source folder | The organization is poor | Use a types/ folder |
Real-World Examples
1. Install the types
npm install --save-dev @types/express
2. The stub
declare module 'legacy-charts' {
export function render(el: HTMLElement): void;
}
3. The tsconfig include
{ "include": ["src", "types"] }
4. The types option
{ "compilerOptions": { "types": ["node", "jest"] } }
5. The skipLibCheck
{ "compilerOptions": { "skipLibCheck": true } }
6. The global augmentation
export {};
declare global {
interface Window { myApp: App; }
}
7. The contribution structure
types/legacy-charts/
index.d.ts
tsconfig.json
legacy-charts-tests.ts
package.json
8. The automatic discovery
import express from 'express';
// The types are from @types/express
9. The scoped types
npm install --save-dev @types/company__library
10. The migration
npm install --save-dev @types/legacy-charts
# Remove the local stub
Visual: The @types Discovery
┌──────────────────────────────────────────────────────────┐
│ import express from 'express' │
│ │ │
│ ▼ │
│ The compiler looks for the types. │
│ │ │
│ ├── The package's own types? │
│ │ (the "types" field in the package.json) │
│ │ │
│ └── The @types package? │
│ node_modules/@types/express/ │
│ index.d.ts │
│ │
│ The @types package is found automatically. │
│ No import or configuration is needed. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Stub
┌──────────────────────────────────────────────────────────┐
│ The library: legacy-charts (no types) │
│ │
│ import { render } from 'legacy-charts'; │
│ │ │
│ └── render is `any` │
│ │
├──────────────────────────────────────────────────────────┤
│ THE STUB │
│ │
│ // types/legacy-charts.d.ts │
│ declare module 'legacy-charts' { │
│ export function render(el: HTMLElement): void; │
│ } │
│ │
│ The import is now checked. │
│ render is typed. │
│ The typo in the argument is a compile error. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Compiler Options
┌──────────────────────────────────────────────────────────┐
│ typeRoots │
│ The directories for the type packages. │
│ Default: ["node_modules/@types"] │
│ │
│ types │
│ The specific packages to load globally. │
│ Default: all the packages in the typeRoots. │
│ │
│ skipLibCheck │
│ Skip the internal declaration checking. │
│ Speeds up the compilation. │
│ The consumer's code is still checked. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Contribution
┌──────────────────────────────────────────────────────────┐
│ DefinitelyTyped repository │
│ │
│ types/ │
│ legacy-charts/ │
│ index.d.ts ← the declarations │
│ tsconfig.json ← the compilation config │
│ legacy-charts-tests.ts ← the tests │
│ package.json ← the metadata │
│ │
│ The pull request is reviewed. │
│ The @types/legacy-charts package is published. │
│ │
│ The consumer installs it. │
│ The import is typed. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| @types scope | The npm scope for the type packages |
| DefinitelyTyped | The source repository |
| Location | node_modules/@types |
| Discovery | Automatic |
| Install | npm install --save-dev @types/<lib> |
| Stub | declare module 'name' {} |
| Options | typeRoots, types, skipLibCheck |
| Contribution | A pull request to DefinitelyTyped |
Key takeaways:
- The
@typespackages are type declarations for JavaScript libraries — they are published from the DefinitelyTyped repository, and they are named@types/<library> - The compiler finds the
@typespackages automatically — it looks innode_modules/@types, and no import or configuration is needed - The
typeRootsoption changes the directories that are searched, and thetypesoption limits the packages — the two control the global type loading - The
skipLibCheckoption skips the internal declaration checking — it speeds up the compilation significantly, and the consumer’s code is still checked - A stub declaration is the fix for a library with no types — the
declare module 'name' {}block describes the parts of the library that are used - The stub is better than casting to
any— it scopes the gap to the functions that are used, and the rest of the code is checked - The stubs belong in a
types/folder that is included in thetsconfig.json— the organization makes them discoverable, and the include makes them part of the compilation - Contributing to DefinitelyTyped means writing the
.d.ts, the tests, and thepackage.json, and opening a pull request — the package is published automatically when the PR is merged - The
@typespackages are a community resource — they are maintained by volunteers, and the quality and the timeliness depend on the community - A TypeScript library that bundles its own types does not need the
@typespackage — thetypesfield in thepackage.jsonpoints to the bundled declarations
Remember: The @types packages are how the TypeScript ecosystem types the JavaScript libraries that do not ship their own types. The DefinitelyTyped project is the source, the @types scope is the distribution, and the compiler finds the packages automatically. When no package exists, a stub declaration is the local fix, and a contribution to DefinitelyTyped is the permanent one. The skipLibCheck option keeps the compilation fast, and the types and typeRoots options control the loading. The types are the community’s gift, and the contribution is the way to give back.
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!