Question
I want to understand how .d.ts declaration files work in TypeScript.
I was told that .d.ts files are similar to .h header files in C and C++, but they do not seem to behave the same way. Right now, I am struggling to understand how to use them correctly.
It seems that I cannot place my .js or .ts implementation code inside a .d.ts file, so it looks like a project may need all three file types: .js, .ts, and .d.ts.
My questions are:
- What is the relationship between these three file types?
- How do I use a
*.d.tsfile? Does it mean I can permanently delete the*.tsfile? - If so, how does a
*.d.tsfile know which JavaScript file it describes?
Short Answer
By the end of this page, you will understand what .d.ts files are, how they differ from .ts and .js files, and when each file type is needed. You will also learn how TypeScript connects declaration files to JavaScript modules and why .d.ts files describe code rather than implement it.
Concept
What a .d.ts file is
A .d.ts file is a TypeScript declaration file. Its job is to describe the shape of code without containing the actual implementation.
That means a declaration file can say:
- a function exists
- what parameters it accepts
- what it returns
- what objects, classes, or constants are available
But it does not contain the executable logic.
The role of each file type
.ts
A .ts file contains TypeScript source code.
- It can contain implementation
- It can contain types
- It is usually compiled into
.js
Example:
export function add(a: number, b: number): number {
return a + b;
}
.js
A .js file contains JavaScript runtime code.
- This is what actually runs
- Browsers and Node.js execute this code
Example:
export function add(a, b) {
return a + b;
}
.d.ts
A .d.ts file contains only type declarations.
- It does not run
- It is used by the TypeScript compiler and editor tooling
- It describes JavaScript or compiled TypeScript code
Example:
export declare function add(a: number, b: number): number;
Why declaration files matter
TypeScript needs type information to give you:
- autocomplete
- error checking
- better refactoring
- documentation-like hints in editors
If a library is written in plain JavaScript, TypeScript cannot automatically know its types. A .d.ts file fills that gap by describing the library.
Common relationships between files
There is not always a 1-to-1 relationship where every project must have all three.
Pattern 1: TypeScript project
You write .ts files.
TypeScript compiles them to .js.
It may also generate .d.ts files.
math.ts -> math.js
-> math.d.ts
Here:
math.tsis the sourcemath.jsis the runtime outputmath.d.tsis the type description for consumers
Pattern 2: JavaScript library with types
You write JavaScript manually and add a .d.ts file.
math.js
math.d.ts
Here:
math.jsrunsmath.d.tsdescribes it
There is no .ts file required.
Pattern 3: TypeScript application only
You write .ts and compile to .js, but you never publish declarations separately.
app.ts -> app.js
A separate .d.ts file may not be necessary at all.
Can .d.ts replace .ts?
Usually, no.
A .d.ts file describes code, but it does not implement code. If you delete your .ts file and only keep the .d.ts, you keep the type information but lose the actual program logic.
This means:
.d.tscan replace.tsonly as a description- it cannot replace
.tsas implementation
If you already have compiled JavaScript somewhere, then a .d.ts file may be enough for TypeScript users to understand the API.
How TypeScript knows which .js file a .d.ts file belongs to
Usually, this is based on the module name or file path.
For example:
lib/
math.js
math.d.ts
If code imports ./math, TypeScript will use math.d.ts for type information and math.js at runtime.
Example:
import { add } from './math';
At development time:
- TypeScript reads
math.d.ts
At runtime:
- JavaScript loads
math.js
The declaration file does not contain a special pointer to the JavaScript file. Instead, the compiler resolves both using the module system and file naming conventions.
Important idea
A .d.ts file is like a contract or interface description for code that exists somewhere else.
It tells TypeScript:
- what names exist
- what types those names have
- how other files may use them
But the actual behavior still comes from .js or compiled .ts output.
Mental Model
Think of these files like a product in a box:
.jsis the actual product you can use.tsis the product being designed with detailed instructions.d.tsis the label on the box describing what is inside
The label is useful, but the label is not the product.
Another way to think about it:
.js= the working machine.d.ts= the blueprint of the machine's public controls.ts= the original source used to build the machine
A blueprint can describe a machine, but it cannot run by itself.
Syntax and Examples
Basic declaration syntax
A declaration file uses declarations instead of implementations.
Function declaration
export declare function greet(name: string): string;
This says:
- there is a function called
greet - it takes a
string - it returns a
string
But there is no function body.
Variable declaration
export declare const version: string;
Interface declaration
export interface User {
id: number;
name: string;
}
Class declaration
export declare class Person {
constructor(name: string);
name: string;
sayHello(): string;
}
Example: JavaScript file plus declaration file
math.js
export function add(a, b) {
return a + b;
}
math.d.ts
export declare function add(a: number, b: number): number;
app.ts
import { add } from './math';
const result = add(2, 3);
console.log(result);
TypeScript understands the types from math.d.ts, while JavaScript executes math.js.
Example: TypeScript source generating declarations
math.ts
export function add(a: number, b: number): number {
return a + b;
}
This can compile to:
generated math.js
export function add(a, b) {
return a + b;
}
generated math.d.ts
export declare function add(a: number, b: number): number;
In this case, you usually write only the .ts file yourself. The compiler can produce the .js and .d.ts files.
Declaring an external module
Sometimes you declare types for a package by module name:
declare module 'my-library' {
export function add(a: number, b: number): number;
}
This tells TypeScript what is available when importing from 'my-library'.
Step by Step Execution
Example
Suppose you have these files:
logger.js
export function log(message) {
return `[LOG] ${message}`;
}
logger.d.ts
export declare function log(message: string): string;
main.ts
import { log } from './logger';
const result = log('Hello');
console.log(result);
What happens step by step
1. TypeScript reads main.ts
It sees:
import { log } from './logger';
So it needs to know what ./logger exports.
2. It looks for type information
TypeScript finds logger.d.ts and reads:
export declare function log(message: string): string;
Now it knows:
logis a function- it expects a string
- it returns a string
3. Type checking happens
This is valid:
const result = log('Hello');
This would be an error:
const result = log(123);
because 123 is not a string.
4. Runtime uses JavaScript
When the program actually runs, TypeScript types are gone. The real executed code comes from logger.js.
That file contains:
export function log(message) {
return `[LOG] ${message}`;
}
5. Output is produced
The result becomes:
[LOG] Hello
Key takeaway
.d.tshelps during development and type checking.jsis what actually runs
Real World Use Cases
1. Adding types to a JavaScript library
Many libraries are written in JavaScript but still want to support TypeScript users. A .d.ts file lets the library provide type information without rewriting the whole codebase in TypeScript.
2. Publishing a TypeScript package
A package author writes .ts source files and publishes:
- compiled
.js - generated
.d.ts
Consumers get both runtime code and type safety.
3. Typing legacy code
In older projects, teams often have lots of JavaScript already. They can gradually add .d.ts files to describe existing modules and improve editor support before fully migrating to TypeScript.
4. Describing browser globals or external APIs
Declaration files can describe values that exist at runtime but are not defined directly in your current source, such as:
- global variables injected by a script
- APIs provided by a host environment
- third-party plugins
5. Internal shared contracts in large codebases
Teams may use declarations to describe public APIs between packages, especially in monorepos or shared libraries.
Real Codebase Usage
Common patterns in real projects
Generate .d.ts from .ts
This is the most common pattern in TypeScript libraries. Developers write normal TypeScript, then let the compiler generate declarations.
Benefits:
- avoids duplicating types manually
- keeps declarations in sync with implementation
- reduces mistakes
Write .d.ts manually for JavaScript code
This is common when:
- a project is still mostly JavaScript
- only type hints are needed
- migration to TypeScript is gradual
Describe package public APIs
Many packages expose only selected functions and classes. Their declaration files describe the exported API clearly, which acts like documentation and type safety at the same time.
Use declarations for globals
Example:
declare const API_URL: string;
This is useful when a bundler, environment, or script injects something at runtime.
Validation and contract clarity
Developers use type declarations to make function usage clearer:
export interface Config {
host: string;
port: number;
}
export declare function connect(config: Config): void;
This helps catch incorrect usage before runtime.
Practical advice
If you own the TypeScript source, prefer writing .ts and generating .d.ts.
If you only have JavaScript implementation, provide .d.ts manually.
Common Mistakes
1. Putting implementation code inside .d.ts
A declaration file should not contain real executable logic.
Incorrect
export function add(a: number, b: number): number {
return a + b;
}
Correct
export declare function add(a: number, b: number): number;
2. Thinking .d.ts can run by itself
A .d.ts file is not runtime code. If you only keep a declaration file and remove the actual JavaScript or compiled output, there is nothing to execute.
3. Letting declarations get out of sync with implementation
math.js
export function add(a, b) {
return String(a + b);
}
wrong math.d.ts
export declare function add(a: number, b: number): number;
This is dangerous because the declaration says number, but the implementation returns a string.
How to avoid it
- generate declarations from
.tswhen possible - update declaration files whenever implementation changes
4. Assuming every file needs a matching .d.ts
Most applications do not need manual declaration files for every module. Often, .ts source files already provide the types.
5. Confusing declare with definition
declare means “this exists somewhere else.” It does not create the value at runtime.
Example
declare const apiKey: string;
console.log(apiKey);
This type-checks only if apiKey truly exists at runtime somewhere else.
6. Using the wrong module shape
If the JavaScript exports do not match the declaration exports, imports will be confusing or broken.
For example, declaring a default export when the JS file uses named exports can cause problems.
Comparisons
.ts vs .js vs .d.ts
| File type | Contains implementation? | Contains types? | Runs directly? | Common use |
|---|---|---|---|---|
.ts | Yes | Yes | No, usually compiled first | Main TypeScript source |
.js | Yes | Sometimes via JSDoc, but not TypeScript syntax | Yes | Runtime code |
.d.ts | No | Yes | No | Type declarations only |
.d.ts vs C/C++ header files
| Concept | C/C++ .h | TypeScript .d.ts |
|---|---|---|
| Describes code structure | Yes | Yes |
| Contains executable implementation | Sometimes inline or via macros/templates | No |
| Used for compile-time information | Yes | Yes |
| Used at runtime | No | No |
| Tied to implementation files | Often paired with .c/.cpp | Often paired with .js or generated from .ts |
The comparison to header files is helpful, but not exact. TypeScript declarations are more specifically about type information and API shape.
Manual .d.ts vs generated .d.ts
| Approach | Best when | Pros | Cons |
|---|---|---|---|
Manual .d.ts | You have JavaScript code | Works without rewriting code | Can get out of sync |
Generated .d.ts | You write TypeScript source | Accurate and maintainable | Requires TypeScript source |
Cheat Sheet
Quick reference
What each file does
.ts= TypeScript source.js= runtime JavaScript.d.ts= type declarations only
Core rule
A .d.ts file describes code. It does not implement code.
Common file relationships
source.ts -> output.js + output.d.ts
or
library.js + library.d.ts
Declaration examples
export declare function greet(name: string): string;
export declare const version: string;
export interface User {
id: number;
name: string;
}
When you need .d.ts
- when typing JavaScript modules
- when publishing types for a library
- when declaring globals or external modules
When you usually do not need manual .d.ts
- when writing a normal TypeScript app and not publishing reusable APIs
- when
.tssource already provides the needed types
Important rules
- no function bodies in
.d.ts declaremeans the real value exists elsewhere- declarations must match runtime behavior
- TypeScript uses module resolution to connect
.d.tsto imports
Remember
If you delete .ts and keep only .d.ts, you keep the type information, not the implementation.
FAQ
What is a .d.ts file in TypeScript?
A .d.ts file is a declaration file that describes the types and exported API of code without containing the actual implementation.
Do I need both .ts and .d.ts files?
Not always. If you write TypeScript, your .ts files may be enough during development, and .d.ts files can be generated when needed. If you write JavaScript, you may add .d.ts files manually.
Can a .d.ts file replace a .ts file?
No, not as implementation. A .d.ts file only provides type information. You still need JavaScript or compiled TypeScript code to run the program.
How does TypeScript match a .d.ts file to a .js file?
Usually through the same module name or file path. For example, importing ./math lets TypeScript use math.d.ts for types and math.js at runtime.
Are .d.ts files only for libraries?
No. They are common in libraries, but they can also be used in applications for globals, legacy JavaScript modules, or third-party integrations.
Should I write .d.ts files by hand?
Only when needed. If your project is written in TypeScript, it is usually better to generate declaration files automatically.
Why can't I put normal code inside a .d.ts file?
Because declaration files are meant only for describing shapes and types. They are not executable modules with implementation logic.
Mini Project
Description
Create a small JavaScript utility module and a matching TypeScript declaration file. This project demonstrates the real purpose of .d.ts files: giving TypeScript type information for JavaScript code that already exists and runs.
Goal
Build a JavaScript module that exports a few utility functions, then add a .d.ts file so a TypeScript file can import and use that module with full type checking.
Requirements
- Create a
utils.jsfile with at least two exported functions. - Create a matching
utils.d.tsfile that describes those exports. - Create a
main.tsfile that imports and uses the JavaScript module. - Make sure the declarations match the real JavaScript behavior.
- Include at least one function that returns a typed object.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.