A practical guide to project-level TypeScript declaration files like typings.d.ts, with examples for assets, globals, and third-party libraries.
If you have used TypeScript for more than a day, you have probably seen errors like:
TS2307: Cannot find module './styles.module.css' or its corresponding type declarations.TS2339: Property 'gtag' does not exist on type 'Window & typeof globalThis'.
Everything still works in the browser, but TypeScript is unhappy.
This is exactly where a project-level declaration file like typings.d.ts comes in.
In this post you will learn:
- What
.d.tsfiles are. - What
typings.d.tsis used for. - The most common things you will declare there.
- Concrete examples you can copy into your own project.
You do not need deep TypeScript knowledge; if you understand basic interface and type, you are good.
1. What is a .d.ts file?
A .d.ts file is a type declaration file. It contains types only, no runtime code.
- Regular
.ts/.tsxfiles:- Compile to JavaScript.
- Contain logic: functions, classes, imports/exports.
.d.tsfiles:- Contain only type information used by the compiler.
- Do not become part of the JavaScript bundle.
You will see them in node_modules/@types/..., but you can also create your own, typically something like:
src/typings.d.tsTypeScript loads it automatically (if it is inside include in tsconfig.json), and everything you declare there is visible throughout the project.
2. Why have a typings.d.ts file?
typings.d.ts is a good place for:
- Telling TypeScript about non-TypeScript files you import. For example images, CSS modules, and other assets.
- Extending global types.
For example adding
window.gtagfor analytics. - Typing libraries that do not ship types.
For example
declare module 'rehype-react' { ... }. - Patching or augmenting existing types.
For example extending
WindoworProcessEnv.
It is your “teach TypeScript about the outside world” file.
3. Declaring modules for non-code imports
3.1. Images and icons
TypeScript only understands .ts, .tsx, .js, and similar file types by default. If you try:
import favicon from './favicon.ico';You will get something like:
TS2307: Cannot find module './favicon.ico' or its corresponding type declarations.To fix it, declare that any *.ico import is a string:
// typings.d.ts
declare module '*.ico' {
const ico: string;
export = ico;
}Now this compiles:
// layout.tsx
import favicon from './favicon.ico';
console.log(favicon); // typed as string, for example "/static/favicon-1234.ico"You can do the same for other asset types.
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.svg' {
const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
export default ReactComponent;
}Usage examples:
import logoUrl from './logo.png';
import LogoIcon from './logo.svg';
<img src={logoUrl} alt="Logo" />;
<LogoIcon width={24} height={24} />;3.2. CSS modules
If you import CSS modules like this:
import styles from './Button.module.css';
<button className={styles.primary}>Click me</button>;TypeScript again does not know what *.module.css is.
In typings.d.ts you can declare a reusable CSSModule type and two module shims:
type CSSModule = Record<string, string>;
declare module '*.module.css' {
const cssModule: CSSModule;
export = cssModule;
}
declare module '*.module.scss' {
const cssModule: CSSModule;
export = cssModule;
}Now:
stylesis typed asRecord<string, string>.styles.primaryis typed asstring.
You get type checking on the keys you use, and TypeScript stops complaining about the import.
4. Extending global objects (like window)
Sometimes you attach things to window (or use libraries that do). For example, Google Analytics 4:
window.gtag('event', 'page_view', { page_path: location.pathname });TypeScript does not know about gtag on window, so you get an error like:
TS2339: Property 'gtag' does not exist on type 'Window & typeof globalThis'.4.1. Augmenting Window
You can augment the built-in Window interface in typings.d.ts:
interface Window {
gtag?: (...args: unknown[]) => void;
}Because interfaces with the same name merge, this adds gtag to the existing Window type.
Now you can safely write:
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
window.gtag('event', 'page_view', {
page_path: window.location.pathname,
});
}TypeScript understands that window.gtag may exist and knows how to call it.
4.2. Another example: storing app config on window
Maybe you have some configuration injected into the page by your backend:
// typings.d.ts
interface Window {
APP_CONFIG?: {
apiBaseUrl: string;
environment: 'development' | 'staging' | 'production';
};
}Now this code is type-safe:
const apiBaseUrl = window.APP_CONFIG?.apiBaseUrl ?? 'http://localhost:3000';Without the declaration, TypeScript would not know what APP_CONFIG is.
5. Adding types for a library that has none
Sometimes you use a library that does not ship TypeScript types, and there is no @types/... package published.
Imagine a simple logging library called cool-logger:
import { logInfo, logError } from 'cool-logger';Without any declaration, TypeScript complains:
TS2307: Cannot find module 'cool-logger' or its corresponding type declarations.You can start with a minimal declaration in typings.d.ts:
declare module 'cool-logger' {
export function logInfo(message: string): void;
export function logError(message: string, error?: unknown): void;
}Now this is fully typed:
import { logInfo, logError } from 'cool-logger';
logInfo('App started');
logError('Something went wrong', new Error('Boom'));If you are not sure about exact signatures, you can also start looser and tighten later:
declare module 'cool-logger' {
export function logInfo(...args: unknown[]): void;
export function logError(...args: unknown[]): void;
}6. A more complex example: rehype-react
Sometimes you want more than “please stop erroring”. You want helpful types for a complex plugin.
Imagine you use rehype-react to turn HTML AST into React components.
In your code you might have:
import unified from 'unified';
import rehypeReact from 'rehype-react';
import { CodeBlock } from './CodeBlock';
const processor = unified().use(rehypeReact, {
createElement: React.createElement,
components: {
code: CodeBlock,
},
});To make this type-safe, you can declare rehype-react in typings.d.ts:
declare module 'rehype-react' {
import type { Plugin } from 'unified';
import type { Root } from 'hast';
import type { ReactElement, ComponentType } from 'react';
type RehypeComponents = Record<string, ComponentType<Record<string, unknown>>>;
type RehypeOptions = {
createElement: (...args: unknown[]) => ReactElement;
Fragment?: ComponentType<Record<string, unknown>>;
components?: RehypeComponents;
};
const rehypeReact: Plugin<[RehypeOptions], Root, ReactElement>;
export default rehypeReact;
}Now TypeScript knows:
components.codemust be a React component.createElementmust return aReactElement.
This helps catch mistakes early and makes your editor autocomplete smarter.
7. Record and other helpers you might see
In the examples above, you saw Record being used:
type CSSModule = Record<string, string>;Record is a built-in utility type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};You can think of it as “an object whose keys are K and whose values are T“.
Examples:
// Keys 'id' | 'name', values are string
type UserSummary = Record<'id' | 'name', string>;
// Equivalent to:
// type UserSummary = { id: string; name: string };
// Map of string keys to numbers
type Scores = Record<string, number>;
const scores: Scores = {
alice: 10,
bob: 15,
};Record is often used in typings.d.ts to describe object-like maps.
8. When you do not need typings.d.ts
You do not need to add declarations when:
- The package ships its own
.d.ts(checkpackage.jsonfor a"types"field). - There is an
@types/package-namenpmpackage you have installed. - You are only using
.ts/.tsxmodules with proper exports.
Use typings.d.ts for gaps in the type system:
- Assets (CSS modules, images, icons).
- Globals (
window,document,process.env, …). - Libraries without types.
- Small patches or augmentations.
9. Best practices for typings.d.ts
- Keep it focused. Only declare what you actually use.
- Prefer precise types over
any. Useunknown,Record<string, unknown>, or specific shapes where you can. - Name it something obvious.
typings.d.tsorglobal.d.tsat the root ofsrc/or the project. - Check it into git. It is part of your contract with TypeScript, just like
tsconfig.json. - Treat it as a public API. Other developers (and your future self) will rely on what is declared there.
Once you understand typings.d.ts, those confusing errors:
- “Cannot find module ‘./something.css’ or its corresponding type declarations.”
- “Property ‘gtag’ does not exist on type ‘Window’.”
stop being mysterious and start looking like simple invitations:
Teach TypeScript about this thing.
FAQ
- When should I create a typings.d.ts file in a TypeScript project?
- Create one when you need to teach TypeScript about things it does not know by default: asset imports like .png or .css files, globals you add to window, or third-party libraries that do not ship their own type definitions.
- What kinds of declarations belong in typings.d.ts?
- Use it for ambient declarations such as module shims for assets, interface augmentations for global objects, and lightweight module declarations for untyped libraries. It should not contain runtime logic, only type information consumed by the compiler.
Welcome to The infinite monkey theorem
Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

