TypeScript declaration files are how TypeScript understands JavaScript libraries.
Let’s say you have a simple JavaScript library, my-math.js:
// my-math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
And you want to use it in your TypeScript project:
// app.ts
import { add } from 'my-math';
const result = add(5, 3); // TypeScript error!
console.log(result);
TypeScript throws an error because it has no idea what my-math is, what functions it exports, or what types those functions expect or return. This is where declaration files (.d.ts) come in. They provide type information for JavaScript code.
Here’s how you’d write a declaration file for my-math.js:
// my-math.d.ts
declare module 'my-math' {
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
}
Now, if you place my-math.d.ts in your project (e.g., in a types folder or at the root, and configure your tsconfig.json to include it), your TypeScript code will work:
// app.ts
import { add } from 'my-math'; // No error!
const result = add(5, 3);
console.log(result); // Output: 8
TypeScript uses these .d.ts files to perform static type checking. When you import { add } from 'my-math', the TypeScript compiler looks for my-math.d.ts. It finds the declare module 'my-math' block, sees that add is exported, expects it to take two number arguments, and return a number. This allows it to check if your usage of add is type-safe.
The most surprising true thing about declaration files is that they don’t contain any executable code. They are purely metadata. When you compile your TypeScript code, the .d.ts files are used for type checking but are effectively stripped away. The actual JavaScript code from my-math.js is what gets executed at runtime.
Let’s see a slightly more complex example. Suppose your JavaScript library exposes a class:
// logger.js
class Logger {
constructor(prefix = 'INFO') {
this.prefix = prefix;
}
log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}
module.exports = Logger;
And you want to use it in TypeScript:
// app.ts
import Logger from 'logger';
const appLogger = new Logger('APP'); // Error!
appLogger.log('Starting up...');
The error arises because TypeScript doesn’t know that logger exports a Logger class, or how to construct it. Here’s the corresponding declaration file:
// logger.d.ts
declare module 'logger' {
class Logger {
constructor(prefix?: string); // The optional prefix is indicated by '?'
log(message: string): void; // 'void' means the function doesn't return a value
}
export default Logger; // 'export default' is used for default imports
}
With this logger.d.ts, the TypeScript code becomes valid. The constructor(prefix?: string) part tells TypeScript that the constructor can be called with an optional string argument. log(message: string): void specifies that the log method takes a string and returns nothing.
You can also declare global variables or modules that aren’t imported using import. For example, if a JavaScript library adds a global function jQuery:
// In some external JS file loaded via a script tag
var jQuery = function(selector) { /* ... */ };
You’d declare it like this:
// globals.d.ts
declare var jQuery: (selector: string) => any; // 'any' is used here as a placeholder for a more specific type if known
Or if a library attaches properties to window:
// globals.d.ts
interface Window {
myGlobalConfig: {
apiKey: string;
timeout: number;
};
}
The core mechanism is declare. You use declare module 'module-name' to wrap code for an external module. Inside, you use declare var, declare let, declare const for variables, declare function for functions, declare class for classes, and declare interface, declare type, declare enum for types.
When writing declarations for existing JavaScript, you often need to infer types. For instance, a function might return a string or undefined. You’d represent this with a union type: string | undefined. If a function accepts an object with optional properties, you’d use ? after the property name in an interface or type alias.
Consider a library that exports a function that returns a promise:
// async-lib.js
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve({ data: 'some data' }), 1000);
});
}
module.exports = { fetchData };
The declaration file would look like this:
// async-lib.d.ts
declare module 'async-lib' {
interface FetchResult {
data: string;
}
export function fetchData(): Promise<FetchResult>;
}
The Promise<FetchResult> tells TypeScript that fetchData returns a Promise that, when resolved, will yield an object conforming to the FetchResult interface.
The most common way to get declaration files for popular libraries is through @types packages. For example, to get declarations for Lodash, you’d install npm install --save-dev @types/lodash. These are community-maintained declarations. However, when using a library that doesn’t have @types support, or for your own internal JavaScript modules, you’ll write them yourself.
A subtle but crucial aspect of declaration files is handling overload signatures. If a JavaScript function can be called in multiple ways with different argument types and return types, you declare each signature on separate lines before the final implementation signature. For example, a function padLeft that can pad a string with a character or a string:
// string-utils.d.ts
declare module 'string-utils' {
export function padLeft(value: string, padding: number): string;
export function padLeft(value: string, padding: string): string;
export function padLeft(value: string, padding: number | string): string {
// This is the implementation signature, often omitted if only overloads are needed
// or if the JS implementation handles all cases.
// For declaration files, you typically just list the overloads.
}
}
When you call padLeft('hello', 5) or padLeft('hello', '--'), TypeScript checks these calls against the declared overload signatures.
The next hurdle you’ll likely face is dealing with ambient modules and how they interact with moduleResolution in your tsconfig.json.