Skip to main content

TypeScript Declare (How to Declare Variables, Functions & Types)

Half the JavaScript ecosystem predates TypeScript. Useful libraries ship without type definitions, build tools inject globals at compile time, and legacy scripts populate window with values TypeScript has never seen. In all these cases, you need a way to tell the compiler what's available at runtime without reimplementing it. That's what declare is for.

The declare keyword draws a map for the TypeScript compiler. It describes code that exists in the runtime environment (a CDN-loaded library, a globally injected variable, or a third-party package without .d.ts files) without generating any JavaScript output. Once declared, TypeScript treats those values as fully typed and checks your code against them.

What is the Declare Keyword in TypeScript?

The declare keyword creates ambient declarations that inform the compiler about types without providing an implementation. Use it for external JavaScript libraries, global variables, and modules that live outside TypeScript's direct reach.

When you use declare, you're telling TypeScript: "Trust me, this exists somewhere else in the runtime environment."

// Inform TypeScript about a global variable defined elsewhere
declare const API_KEY: string;

// Now you can use it without TypeScript errors
console.log(API_KEY);

No JavaScript is emitted for declare statements. They exist purely at the type level.

TypeScript Declare Variants: const, let, var, function, class, and enum

The declare keyword works with several TypeScript constructs, and the choice between them matters.

declare const, let, and var

// Use declare const when the global won't be reassigned
declare const ENV: 'development' | 'production' | 'test';

// Use declare let when the value can change at runtime
declare let currentUser: string | null;

// Use declare var for legacy globals with function-level scoping (rare in modern code)
declare var legacyConfig: Record<string, unknown>;

In practice, declare const is the right choice for most global variables like API keys, environment strings, or injected configuration. Reach for declare let only when you know the value mutates.

declare function

// Declare a global function provided by an external script
declare function trackEvent(category: string, action: string): void;

// Call it with full type safety
trackEvent('button', 'click');

declare class

// Declare a class provided by a globally-loaded script
declare class Analytics {
constructor(trackingId: string);
sendEvent(event: string, data?: Record<string, unknown>): void;
}

const analytics = new Analytics('UA-12345');
analytics.sendEvent('page_view');

declare enum

// Declare an enum from a globally-loaded constants file
declare enum LogLevel {
Debug,
Info,
Warning,
Error,
}

For more on enums, see TypeScript Enums.

Declaring Global Variables with declare

The most common use of declare is defining global variables that exist at runtime but aren't recognized by TypeScript.

Basic Global Variable Declaration

// Declare a global variable from a third-party script
declare const googleAnalytics: {
trackEvent(category: string, action: string, label?: string): void;
};

// Use it without TypeScript errors
googleAnalytics.trackEvent("button", "click", "login-button");

Extending Global Objects

You can also use declare to add properties to existing global objects:

// Add a custom property to the Window interface
declare global {
interface Window {
myCustomLibrary: {
initialize(): void;
processData(data: unknown): void;
}
}
}

// Now TypeScript recognizes this property on the window object
window.myCustomLibrary.initialize();

For complex global objects, TypeScript interfaces let you define their structure more precisely.

Creating Type Definitions for Third-Party Libraries

When a library doesn't ship with TypeScript definitions, you can write your own using declare.

Creating Declaration Files (.d.ts)

Declaration files use the .d.ts extension and contain type information without implementations:

// file: mylib.d.ts
declare module 'my-js-library' {
export function doSomething(value: string): number;
export function processData(data: object): void;
export const VERSION: string;
}

Then import and use the library as if it had native TypeScript support:

import { doSomething, VERSION } from 'my-js-library';

console.log(doSomething("test")); // TypeScript knows this returns a number
console.log(VERSION); // TypeScript knows this is a string

Declaring Library Functions and Objects

For more complex libraries, declare specific functions or objects directly:

// Declare a jQuery-like library
declare function $(selector: string): {
on(event: string, callback: (e: Event) => void): void;
css(property: string, value: string): void;
html(content: string): void;
};

// Now you can use it with type safety
$(".button").on("click", (e) => {
$(e.target as Element).css("color", "red");
});

Where the compiler needs a nudge about specific types inside your declarations, reach for TypeScript type assertion.

How TypeScript Discovers Declaration Files

TypeScript automatically picks up .d.ts files that fall within your tsconfig.json include paths. If your declaration file isn't being recognized, that's usually the first place to check.

{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src/**/*", "types/**/*"]
}

A common convention is to create a types/ folder at the project root for your custom declaration files. For global declarations, a globals.d.ts file in your src/ folder is a clean approach:

project/
├── src/
│ ├── globals.d.ts ← global declare statements go here
│ └── index.ts
├── types/
│ └── my-js-library.d.ts ← third-party module declarations
└── tsconfig.json

If you're using @types packages from DefinitelyTyped, TypeScript discovers them automatically. The types and typeRoots options in tsconfig.json let you restrict which @types packages are loaded, which can speed up compilation in large projects.

Declaring Module Augmentation

TypeScript allows you to add new functionality to existing modules using declaration merging. One of the most common real-world uses is augmenting the Express Request type to include authenticated user data:

// types/express.d.ts
import { User } from '../src/models/user';

declare global {
namespace Express {
interface Request {
user?: User;
}
}
}

// This export is required — without it, the file is treated as a script
// and declare global won't work
export {};

With this in place, TypeScript recognizes req.user across your entire Express app:

import { RequestHandler } from 'express';

const requireAuth: RequestHandler = (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};

Module augmentation works the same way for any library. Here's a more general pattern that adds properties to an existing module:

// Original module
import { User } from 'user-library';

// Augment the module with new properties
declare module 'user-library' {
interface User {
// Add new properties to existing interface
lastLoginDate: Date;
isAdmin: boolean;
}

// Add new function to the module
export function getUserSettings(user: User): UserSettings;
}

// Now you can use the augmented types
const user: User = getUser();
console.log(user.lastLoginDate); // TypeScript recognizes the new property
const settings = getUserSettings(user);

Module augmentation lets you extend existing libraries without touching their source, the same way Convex's API generation works with TypeScript modules.

Defining Ambient Modules with declare module

You can define types for entire modules that don't have TypeScript definitions:

// Define an ambient module for a library without types
declare module 'chart-library' {
export class Chart {
constructor(element: HTMLElement, options?: ChartOptions);
update(data: ChartData): void;
destroy(): void;
}

export interface ChartData {
labels: string[];
datasets: Dataset[];
}

export interface Dataset {
label: string;
data: number[];
backgroundColor?: string;
}

export interface ChartOptions {
responsive?: boolean;
maintainAspectRatio?: boolean;
}
}

// Now you can import and use it with type safety
import { Chart } from 'chart-library';

const chart = new Chart(document.getElementById('chart')!, {
responsive: true
});

The same approach works for any library that ships without types. If you're building with Convex's server modules, you'd follow this same pattern.

Declaring Interfaces for External JavaScript Libraries

For external libraries, you can create interfaces that match their structure:

// Declare interfaces for a JavaScript mapping library
declare namespace MapLibrary {
interface MapOptions {
center: [number, number];
zoom: number;
interactive?: boolean;
}

interface Map {
setCenter(coordinates: [number, number]): void;
setZoom(level: number): void;
addMarker(coordinates: [number, number], options?: MarkerOptions): Marker;
}

interface MarkerOptions {
color?: string;
draggable?: boolean;
popup?: string;
}

interface Marker {
setPopup(content: string): void;
remove(): void;
}
}

declare const MapLib: {
createMap(element: HTMLElement, options: MapLibrary.MapOptions): MapLibrary.Map;
};

Namespaces keep these interfaces grouped logically. It's worth using once your declarations start to grow.

Best Practices for Using the declare Keyword

A few rules worth following:

  1. Check DefinitelyTyped first -- search for @types/your-library on npm before writing your own declarations. Someone has probably already done the work.
  2. Use declare const by default. Only reach for declare let or declare var when the global value genuinely changes.
  3. Document your declarations with JSDoc comments for better IDE integration.
  4. Keep your declaration files separate from implementation files.
  5. Start with partial declarations -- use optional properties for API surface you're unsure about, then fill in the gaps as you use the library.

Where Things Go Wrong

Forgetting export {} in Module Files

This is the most common declare global mistake. If your file has no imports or exports, TypeScript treats it as a script and declare global works fine. But as soon as you add any import statement, the file becomes a module and declare global requires export {} at the bottom.

// BAD: This file has an import, so declare global won't work without export {}
import { User } from './models';

declare global {
interface Window {
currentUser: User; // TypeScript may not pick this up
}
}

// GOOD: Add export {} to make this a module and enable declare global
import { User } from './models';

declare global {
interface Window {
currentUser: User;
}
}

export {};

Confusing Ambient Declarations with Real Code

declare never emits JavaScript. If you write declare const config = { apiUrl: '...' }, that value doesn't exist at runtime. TypeScript thinks it does, but the actual implementation has to come from somewhere else: a script tag, a build tool like Webpack's DefinePlugin, or a server-injected variable. Make sure whatever you're declaring is actually being provided before your code runs.

Conflicts with @types Packages

If you install @types/some-library and also have a custom declaration file for the same library, TypeScript might see duplicate or conflicting definitions. Check whether an @types package exists before writing your own, and remove your custom file if you switch to the official types.

Declaration File Not Being Picked Up

If TypeScript isn't seeing your .d.ts file, run through this checklist:

  • It's covered by your tsconfig.json include pattern
  • The file doesn't accidentally contain import/export statements that require export {} for global declarations to work
  • You're not relying on skipLibCheck: true to silently ignore declaration errors

TypeScript Declarations: Key Takeaways

The declare keyword connects JavaScript's runtime world to TypeScript's type system. A few rules to keep in mind:

  • Use declare const for globals that don't change, declare let for ones that do
  • Put global declarations in a globals.d.ts file, covered by your tsconfig.json include paths
  • Use declare module for third-party libraries without types, and search @types first
  • Remember export {} when mixing declare global with module imports
  • Module augmentation lets you safely extend existing types without modifying the originals

Whether you're creating type definitions for third-party libraries, declaring global variables, or augmenting existing modules, declare gives you the flexibility to work with both typed and untyped code in the same project.