Bird
Raised Fist0
Typescriptprogramming~15 mins

Writing custom declaration files in Typescript - Deep Dive

Choose your learning style10 modes available
Overview - Writing custom declaration files
What is it?
Writing custom declaration files means creating special files that tell TypeScript about the types and shapes of code that doesn't have built-in type information. These files use the .d.ts extension and describe variables, functions, classes, or modules so TypeScript can check your code for errors. They help TypeScript understand JavaScript libraries or code without types. This way, you get safety and better coding tools even when using untyped code.
Why it matters
Without declaration files, TypeScript can't check or understand code from plain JavaScript or third-party libraries without types. This means you lose the benefits of catching mistakes early and getting helpful hints while coding. Custom declaration files fill this gap, making your code safer and easier to work with. They let you use any JavaScript code confidently with TypeScript's powerful checks.
Where it fits
Before writing custom declaration files, you should know basic TypeScript types and how TypeScript checks code. You should also understand modules and importing. After learning this, you can explore publishing your declaration files for others or advanced typing techniques like declaration merging and conditional types.
Mental Model
Core Idea
A custom declaration file is a map that tells TypeScript the shape and types of code it can't see, so it can check your code safely.
Think of it like...
It's like writing a recipe card for a dish you didn't cook yourself, so anyone can follow it without guessing the ingredients or steps.
┌───────────────────────────────┐
│       Custom Declaration      │
│           File (.d.ts)        │
├──────────────┬────────────────┤
│ Describes    │ Code without   │
│ types &     │ types (JS lib) │
│ shapes      │                │
├──────────────┴────────────────┤
│ Enables TypeScript to check    │
│ your code safely using this    │
│ type information               │
└───────────────────────────────┘
Build-Up - 7 Steps
1
FoundationWhat are declaration files
🤔
Concept: Introduction to .d.ts files and their purpose.
Declaration files (.d.ts) are special TypeScript files that only contain type information. They do not produce JavaScript code but tell TypeScript about the types of variables, functions, classes, or modules. For example, if you have a JavaScript library without types, you can write a declaration file to describe its API so TypeScript can check your usage.
Result
You understand that declaration files provide type info without implementation.
Knowing that declaration files separate type info from code helps you see how TypeScript can check untyped code safely.
2
FoundationBasic syntax of declaration files
🤔
Concept: Learn how to write simple type declarations in .d.ts files.
In a declaration file, you use keywords like declare to describe variables, functions, or classes. For example: declare const version: string; declare function greet(name: string): void; declare class Person { constructor(name: string); sayHello(): void; } This tells TypeScript what types these items have without giving code.
Result
You can write simple declarations describing variables, functions, and classes.
Understanding the declare keyword is key because it tells TypeScript to expect these types elsewhere.
3
IntermediateDeclaring modules and namespaces
🤔
Concept: How to describe external modules or global namespaces in declaration files.
When a JavaScript library is imported as a module, you declare it using module syntax: declare module 'my-lib' { export function doThing(x: number): string; } For global scripts, you can use namespaces: declare namespace MyLib { function doThing(x: number): string; } This tells TypeScript how to find types when importing or using globals.
Result
You can write declarations for both module-based and global libraries.
Knowing how to declare modules vs namespaces helps you match the library's usage style.
4
IntermediateDescribing complex types and interfaces
🤔Before reading on: do you think you can describe an object with optional and readonly properties in a declaration file? Commit to your answer.
Concept: Learn to write interfaces and types with optional, readonly, and function properties.
You can describe objects with interfaces: interface User { readonly id: number; name: string; age?: number; // optional greet(): void; } Then declare variables or functions using these interfaces: declare const currentUser: User; This helps TypeScript understand complex data shapes.
Result
You can describe detailed object shapes and function signatures.
Mastering interfaces in declaration files lets you model real-world data precisely.
5
IntermediateUsing declaration merging and augmentation
🤔Before reading on: do you think you can add new properties to an existing module's types without changing its original code? Commit to your answer.
Concept: Declaration merging lets you extend existing types or modules by writing new declarations with the same name.
If a library lacks some types, you can add them: // Original declaration // declare module 'my-lib' { // function doThing(): void; // } // Your augmentation declare module 'my-lib' { function newFeature(): number; } TypeScript merges these declarations, so your additions become part of the module's types.
Result
You can safely extend or fix types from third-party libraries.
Understanding declaration merging is powerful for customizing types without forking code.
6
AdvancedWriting declaration files for JavaScript code
🤔Before reading on: do you think you can write a declaration file for a JavaScript file that exports multiple functions and variables? Commit to your answer.
Concept: Learn how to write declaration files that match JavaScript code exports, including default and named exports.
For a JS file like: // utils.js // export function add(a, b) { return a + b; } // export const version = '1.0'; You write a declaration file: declare module 'utils' { export function add(a: number, b: number): number; export const version: string; } This lets TypeScript know what the JS code provides.
Result
You can create declaration files that describe real JavaScript modules.
Knowing how to mirror JS exports in declarations bridges untyped and typed code.
7
ExpertHandling tricky cases and advanced types
🤔Before reading on: do you think declaration files can describe dynamic or conditional types? Commit to your answer.
Concept: Explore advanced TypeScript features like conditional types, generics, and mapped types in declaration files to describe complex APIs.
Declaration files can use all TypeScript type features: interface ApiResponse { data: T; error?: string; } declare function fetchData(url: string): Promise>; You can also use conditional types: type Id = T extends { id: infer U } ? U : never; These let you describe very flexible and powerful APIs.
Result
You can write declaration files that describe complex, dynamic types.
Mastering advanced types in declarations unlocks full power to type any JavaScript API.
Under the Hood
Declaration files are parsed by the TypeScript compiler as pure type information without generating JavaScript code. When you import or use code described by a declaration file, TypeScript uses the types to check your code for errors and provide editor features like autocomplete. The compiler merges declaration files with your code's types to create a complete type system view.
Why designed this way?
Declaration files separate type information from implementation to allow gradual typing of JavaScript code. This design lets TypeScript add types to existing JavaScript libraries without changing their code. It also keeps compilation fast by skipping code generation for declarations. Alternatives like embedding types inline would require rewriting or recompiling libraries.
┌───────────────┐       ┌───────────────┐
│ JavaScript    │       │ Declaration   │
│ Code (no     │       │ File (.d.ts)  │
│ types)       │       │               │
└──────┬────────┘       └──────┬────────┘
       │                       │
       │ Used by TypeScript     │
       │ compiler for types    │
       ▼                       ▼
┌─────────────────────────────────────┐
│ TypeScript Compiler                  │
│ - Merges declarations with code     │
│ - Checks types                      │
│ - Provides editor support           │
└─────────────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do declaration files contain executable JavaScript code? Commit to yes or no.
Common Belief:Declaration files contain JavaScript code that runs at runtime.
Tap to reveal reality
Reality:Declaration files only contain type information and no executable code; they are ignored during JavaScript execution.
Why it matters:Thinking declaration files run code can cause confusion about performance and debugging, leading to misuse or expecting runtime effects.
Quick: Can you write declaration files that fix or extend types of third-party libraries without modifying their source? Commit to yes or no.
Common Belief:You must change the original library code to fix or add types.
Tap to reveal reality
Reality:You can write separate declaration files that augment or override types without touching the original code.
Why it matters:Believing you must edit libraries limits your ability to safely customize types and slows development.
Quick: Do declaration files automatically infer types from JavaScript code? Commit to yes or no.
Common Belief:Declaration files automatically get types from JavaScript code without manual writing.
Tap to reveal reality
Reality:Declaration files must be written manually or generated by tools; they do not infer types automatically.
Why it matters:Expecting automatic inference can lead to missing or incorrect types and false confidence in type safety.
Quick: Can declaration files describe runtime behavior like function implementation? Commit to yes or no.
Common Belief:Declaration files describe how functions work internally.
Tap to reveal reality
Reality:Declaration files only describe types and signatures, not how functions behave at runtime.
Why it matters:Confusing types with behavior can cause misunderstandings about what TypeScript checks and what it doesn't.
Expert Zone
1
Declaration files can use triple-slash directives to reference other declaration files, enabling modular type definitions.
2
Ambient declarations inside .d.ts files are global by default unless wrapped in modules, which affects scope and usage.
3
Declaration merging can combine interfaces, namespaces, and modules in subtle ways that allow powerful type extensions but can cause conflicts if misunderstood.
When NOT to use
Custom declaration files are not ideal when a library already provides official, well-maintained types or when automatic type generation tools can produce accurate types. In such cases, prefer using existing types or tools like TypeScript's --declaration flag or third-party generators to avoid maintenance overhead.
Production Patterns
In production, teams often write custom declaration files to add missing types for internal or third-party JavaScript code. They maintain these files alongside code or publish them as @types packages. Declaration merging is used to extend library types safely. Advanced types like generics and conditional types model complex APIs, ensuring robust type safety across large codebases.
Connections
TypeScript Generics
Builds-on
Understanding generics helps you write flexible declaration files that describe APIs working with many types, making your declarations reusable and precise.
API Documentation
Similar pattern
Declaration files act like formal API documentation for code, specifying what inputs and outputs look like, which helps both humans and tools understand usage.
Interface Contracts in Law
Analogy in a different field
Just like legal contracts define obligations without showing actions, declaration files define type contracts without implementation, ensuring everyone agrees on how code should be used.
Common Pitfalls
#1Writing declaration files that include implementation code.
Wrong approach:declare function greet(name: string) { console.log('Hello ' + name); }
Correct approach:declare function greet(name: string): void;
Root cause:Confusing declaration files with regular TypeScript files that contain code; declaration files only describe types.
#2Not wrapping declarations in modules when describing module-based libraries.
Wrong approach:declare function doThing(x: number): string;
Correct approach:declare module 'my-lib' { export function doThing(x: number): string; }
Root cause:Misunderstanding module scope causes declarations to be global and unusable when importing.
#3Forgetting to mark optional properties with '?' in interfaces.
Wrong approach:interface User { name: string; age: number; } // age is optional in reality
Correct approach:interface User { name: string; age?: number; }
Root cause:Not recognizing optional properties leads to incorrect type errors when properties are missing.
Key Takeaways
Custom declaration files let TypeScript understand code without built-in types by describing its shape and types separately.
They use the declare keyword and special syntax to describe variables, functions, classes, modules, and namespaces.
Declaration merging allows safe extension of existing types without modifying original code, enabling flexible type customization.
Advanced TypeScript features like generics and conditional types can be used in declaration files to describe complex APIs.
Writing correct declaration files improves code safety, tooling, and developer confidence when using untyped JavaScript libraries.

Practice

(1/5)
1. What is the main purpose of writing a custom declaration file in TypeScript?
easy
A. To tell TypeScript about types in JavaScript or untyped libraries
B. To write JavaScript code that runs faster
C. To convert TypeScript code into JavaScript
D. To create new JavaScript functions

Solution

  1. Step 1: Understand the role of declaration files

    Declaration files provide type information about JavaScript code or libraries without types.
  2. Step 2: Identify the purpose in TypeScript

    This helps TypeScript check code and improve editor support by knowing types.
  3. Final Answer:

    To tell TypeScript about types in JavaScript or untyped libraries -> Option A
  4. Quick Check:

    Custom declaration files = type info for JS [OK]
Hint: Declaration files describe types for untyped JS code [OK]
Common Mistakes:
  • Thinking declaration files speed up code execution
  • Confusing declaration files with code transpilation
  • Believing declaration files create new functions
2. Which syntax correctly declares a module named 'my-lib' in a custom declaration file?
easy
A. module declare 'my-lib' { function foo(): void; }
B. declare module 'my-lib' { export function foo(): void; }
C. declare 'my-lib' module { export function foo(): void; }
D. module 'my-lib' declare { export function foo(): void; }

Solution

  1. Step 1: Recall correct module declaration syntax

    TypeScript uses declare module 'name' { ... } to declare modules.
  2. Step 2: Check each option's syntax

    Only declare module 'my-lib' { export function foo(): void; } uses the correct order and keywords.
  3. Final Answer:

    declare module 'my-lib' { export function foo(): void; } -> Option B
  4. Quick Check:

    Correct module syntax = declare module 'name' [OK]
Hint: Use 'declare module' followed by module name in quotes [OK]
Common Mistakes:
  • Swapping 'declare' and 'module' keywords
  • Omitting quotes around module name
  • Using incorrect keyword order
3. Given this declaration file snippet:
declare module 'utils' {
  export function greet(name: string): string;
}

What is the correct TypeScript output for greet('Alice') if implemented as return `Hello, ${name}!`;?
medium
A. "Hello, Alice!"
B. Error: greet is not defined
C. greet(Alice)
D. Hello, Alice!

Solution

  1. Step 1: Understand the declaration and implementation

    The declaration says greet takes a string and returns a string. The implementation returns a template string greeting.
  2. Step 2: Evaluate the output of greet('Alice')

    Calling greet('Alice') returns the string Hello, Alice! without quotes in the output.
  3. Final Answer:

    Hello, Alice! -> Option D
  4. Quick Check:

    Function returns greeting string = Hello, Alice! [OK]
Hint: String return does not include quotes in output display [OK]
Common Mistakes:
  • Including quotes around string output incorrectly
  • Confusing function call with output text
  • Assuming runtime error without implementation
4. Identify the error in this custom declaration file snippet:
declare module 'math-lib' {
  export function add(a: number, b: number): number
}
medium
A. Missing semicolon after function declaration
B. Wrong module name syntax
C. Function parameters must be optional
D. Cannot export functions in declaration files

Solution

  1. Step 1: Check syntax inside module declaration

    Each statement inside declaration files must end with a semicolon.
  2. Step 2: Identify missing semicolon

    The function declaration line lacks a semicolon at the end.
  3. Final Answer:

    Missing semicolon after function declaration -> Option A
  4. Quick Check:

    Declaration statements need semicolons [OK]
Hint: Always end declarations with semicolons [OK]
Common Mistakes:
  • Forgetting semicolons in declaration files
  • Thinking exports are not allowed
  • Misnaming modules with wrong quotes
5. You want to add a global type for a new property window.myAppVersion in a TypeScript project. Which custom declaration file snippet correctly adds this global property?
hard
A. declare module 'global' { interface Window { myAppVersion: string; } }
B. declare module 'window' { export const myAppVersion: string; }
C. declare global { interface Window { myAppVersion: string; } } export {};
D. interface Window { myAppVersion: string; }

Solution

  1. Step 1: Understand how to add global types

    Use declare global block to augment global interfaces like Window.
  2. Step 2: Check each option for correct syntax

    declare global { interface Window { myAppVersion: string; } } export {}; correctly declares global and extends Window interface, then exports empty to make it a module.
  3. Final Answer:

    declare global { interface Window { myAppVersion: string; } } export {}; -> Option C
  4. Quick Check:

    Global augmentation uses declare global + interface extension [OK]
Hint: Use 'declare global' to add global types safely [OK]
Common Mistakes:
  • Trying to declare global types inside a module
  • Omitting export {} to make file a module
  • Declaring interface without 'declare global'
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard