Skip to main content

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

Working with TypeScript often involves interfacing with JavaScript libraries, global variables, or modules that TypeScript needs to be aware of without their actual implementation. The declare keyword helps bridge this gap, allowing you to create type definitions for code that exists elsewhere. This article explains how to use declare for ambient declarations, global variables, type definitions, module augmentations, and more.

What is the Declare Keyword in TypeScript?

The declare keyword in TypeScript creates ambient declarations that inform the compiler about types without providing an implementation. This is particularly useful when working with external JavaScript libraries, global variables, or modules that exist outside your TypeScript code.

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);

Declaring Global Variables with declare

One common use of the declare keyword is to define global variables that exist in your JavaScript 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: any): void;
}
}
}

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

When working on complex applications, you might need to use TypeScript interfaces to define the structure of these global objects more precisely.

Creating Type Definitions for Third-Party Libraries

When using JavaScript libraries without TypeScript definitions, you can create your own type definitions 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, you might need to declare specific functions or objects:

// 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).css("color", "red");
});

Sometimes you'll need to use TypeScript type assertion to help the compiler understand specific types within these declarations.

Declaring Module Augmentation

TypeScript allows you to add new functionality to existing modules using declaration merging:

// 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 is a powerful feature that lets you extend existing libraries, similar to how 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
});

This technique is useful when integrating third-party libraries into your application, much like how you'd work with Convex's server modules in a TypeScript project.

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;
};

Using TypeScript namespaces can help organize these interfaces into logical groups, making your code more maintainable.

Best Practices for Using the declare Keyword

When using the declare keyword in TypeScript, follow these best practices:

  1. Only declare what you need - don't create overly complex declarations for simplicity
  2. Document your declarations with JSDoc comments for better IDE integration
  3. Look for existing type definitions on DefinitelyTyped (@types packages) before creating your own
  4. Keep your declaration files separate from implementation files
  5. Validate your declarations by using them in actual code

Common Challenges and Solutions

Missing Type Information

When you're using a library without complete TypeScript definitions, you might not know all the available properties or methods. In these cases:

  1. Use browser developer tools to inspect the object structure at runtime
  2. Check the library's documentation for API references
  3. Use partial type definitions with optional properties
declare namespace LegacyLibrary {
interface Api {
// Properties we know exist
version: string;
initialize(): void;

// Properties we're not sure about
debug?: boolean;
options?: Record<string, unknown>;
}
}

Type Definition Conflicts

Sometimes your custom type definitions might conflict with existing definitions or with runtime behavior:

  1. Use module augmentation for gradual refinement of types
  2. Create union types to accommodate multiple possible structures
  3. Use conditional types for adaptive typing

Final Thoughts on TypeScript Declarations

The declare keyword is an essential tool for working with JavaScript libraries in TypeScript projects. By properly defining ambient declarations, you can ensure type safety while leveraging the vast ecosystem of JavaScript libraries.

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.