Common Declaration File Mistakes
Avoid the most common mistakes when writing TypeScript declaration files. Learn the right patterns for types, callbacks, overloads, and module structure.
Declaration files are easy to write but also easy to get wrong. This article covers the most common declaration file mistakes in TypeScript, since a mistake in a declaration file silently undermines type safety for everyone who uses that package, and shows the correct pattern for each one.
Mistake 1: Using Boxed Types Instead of Primitives
TypeScript has two versions of familiar type names: lowercase primitives (number, string, boolean) and uppercase boxed objects (Number, String, Boolean). The boxed versions refer to JavaScript's wrapper objects, which are almost never what you want.
// Wrong
declare function reverse(input: String): String;
// Correct
declare function reverse(input: string): string;The boxed types come from wrapping a primitive in its constructor, which almost never happens in real code but is still a legal value under the uppercase type.
const boxed = new String("hello");
// boxed is String (object wrapper)
const primitive = "hello";
// primitive is stringNobody passes new String("hello") to a function in normal code. Use the lowercase primitives.
The same rule applies to Object vs object. Use lowercase object (added in TypeScript 2.2) for non-primitive values. Avoid uppercase Object entirely, it matches almost everything and provides no useful type narrowing.
Mistake 2: Using any in Declaration Files
any disables type checking. In a declaration file whose entire purpose is to provide type information, using any defeats the point.
// Wrong -- disables checking for anything passed to this function
declare function process(data: any): any;
// Better -- unknown forces the caller to narrow the type
declare function process(data: unknown): Processed;
// Best -- provide the actual type if you know it
declare function process(data: InputData): Processed;unknown is the safe alternative. It says "I do not know the type" without disabling checking. Callers must narrow unknown before using it, which catches mistakes.
The only valid use of any in declaration files is during JavaScript-to-TypeScript migration, where you are intentionally deferring type work. For published types, avoid any.
Mistake 3: Using any as a Callback Return Type
When a callback's return value is ignored by the caller, use void as the return type, not any.
// Wrong
declare function forEach<T>(
items: T[],
callback: (item: T) => any
): void;
// Correct
declare function forEach<T>(
items: T[],
callback: (item: T) => void
): void;The difference matters at the call site. With any, nothing stops a caller from writing const result = forEach(...) and treating result as a real value, even though forEach returns nothing useful. With void, that same assignment is still allowed syntactically, but TypeScript blocks any attempt to actually use the value in a meaningful way, since void signals that the return value is discarded.
Mistake 4: Making Callback Parameters Optional
Callback parameters should not be marked optional unless the caller genuinely might omit them at runtime.
// Wrong -- suggests the callback might receive 1 or 2 arguments
declare function fetch(
url: string,
callback: (data: unknown, elapsed?: number) => void
): void;
// Correct -- the callback always receives both arguments
declare function fetch(
url: string,
callback: (data: unknown, elapsed: number) => void
): void;A callback that accepts fewer arguments is always compatible with a function that passes more arguments. So making elapsed optional is unnecessary, it is always legal to provide a callback that ignores the second parameter:
// This works with the non-optional declaration too
fetch("/api", (data) => {
console.log(data); // Ignores elapsed -- perfectly fine
});Optional callback parameters also create problems with strict null checking, where undefined might unexpectedly satisfy the optional parameter.
Mistake 5: Overload Ordering
TypeScript resolves overloaded functions by picking the first matching signature. Putting generic overloads before specific ones hides the specific overloads.
// Wrong -- unknown matches everything, so HTMLDivElement is never reached
declare function createElement(tag: unknown): unknown;
declare function createElement(tag: HTMLDivElement): string;
const el = createElement(myDiv); // type is unknown, not stringReordering the same two signatures, most specific first, changes which overload TypeScript matches, and the return type changes along with it.
// Correct -- specific overloads first
declare function createElement(tag: HTMLDivElement): string;
declare function createElement(tag: HTMLElement): number;
declare function createElement(tag: unknown): unknown;
const el = createElement(myDiv); // type is stringOrder overloads from most specific to most general. The most constrained parameter types go first. The catch-all, unknown or any, goes last.
Mistake 6: Multiple Overloads That Differ Only in Trailing Parameters
When overloads differ only in the number of trailing parameters, use optional parameters instead of multiple signatures.
// Wrong -- three overloads for what should be one signature
declare function diff(one: string): number;
declare function diff(one: string, two: string): number;
declare function diff(one: string, two: string, three: boolean): number;
// Correct -- one signature with optional parameters
declare function diff(one: string, two?: string, three?: boolean): number;This collapsing is only valid when all overloads have the same return type. If the return type differs based on parameters, separate overloads are appropriate.
Mistake 7: Overloads That Only Differ by One Parameter's Type
When overloads differ only in the type of one parameter and have the same return type, use a union type.
// Wrong -- separate overloads for each type
declare function format(value: string): string;
declare function format(value: number): string;
// Correct -- union type in one signature
declare function format(value: string | number): string;This is especially important when the function is used as a callback or the argument is passed through from another function. Union types compose better than overloads in these scenarios.
Mistake 8: Unused Generic Type Parameters
A generic type parameter that is never used in the type is a red flag.
// Wrong -- T is declared but never used
interface Container<T> {
value: string;
}There are two valid fixes, depending on what the interface is actually meant to describe. If the container should hold a value of the generic type, use T in the member that needs it.
// Either use T
interface Container<T> {
value: T;
}If Container genuinely never varies by type, drop the generic parameter entirely instead of leaving it unused. A generic parameter that never affects the shape of the type is confusing and adds no value for consumers.
// Or drop T if it truly isn't needed
interface Container {
value: string;
}Unused type parameters confuse consumers who try to provide a type argument that has no effect. TypeScript does not enforce this, but it is a code smell.
Mistake 9: Using /// <reference path="..." />
The reference path directive creates file-path-based dependencies that break when the project structure changes.
// Wrong -- fragile file path
/// <reference path="../typescript/lib/typescriptServices.d.ts" />
// Correct -- package-based reference
/// <reference types="typescript" />The types form references a package by name, which npm resolves. It is portable and survives project reorganization. The path form is a legacy mechanism that should not appear in published declaration files.
For the full list of publishing pitfalls, see publish type declarations for a package.
Mistake 10: Forgetting That Declarations Are Erased
Declaration files produce no runtime code. This means you cannot put runtime logic in a declaration file and you cannot rely on types existing at runtime.
// Wrong -- implementation in a declaration file
declare function greet(name: string): string {
return `Hello, ${name}!`;
}
// Correct -- declaration only (implementation lives in .js)
declare function greet(name: string): string;Similarly, do not use TypeScript-specific constructs that have no runtime equivalent, like enum with declare, unless you fully understand the emit behavior.
For a broader understanding of how declaration files fit into the TypeScript ecosystem, see declaration files in TypeScript.
Rune AI
Key Insights
- Use number, string, boolean (lowercase), not Number, String, Boolean (uppercase boxed types).
- Avoid any in declaration files; use unknown or the actual type instead.
- Use void (not any) as the return type for callbacks whose return value is ignored.
- Put specific overloads before general ones so TypeScript picks the right signature.
- Prefer optional parameters over multiple overloads that only differ in trailing parameters.
- Never leave unused generic type parameters in interfaces or types.
Frequently Asked Questions
Why should I avoid Number, String, and Boolean in declaration files?
Why is any dangerous in declaration files?
Does overload ordering really matter?
Conclusion
Writing good declaration files is as much about avoiding mistakes as it is about knowing the syntax. The most common errors -- boxed types, any abuse, callback misconfiguration, and overload ordering -- are easy to fix once you know the patterns. Apply these rules and your declaration files will be safer and more useful.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.