Comparing JavaScript to an example DTS

Common CommonJS Patterns

A module using CommonJS patterns uses module.exports to describe the exported values. For example, here is a module which exports a function and a numerical constant:

  1. const maxInterval = 12;
  2. function getArrayLength(arr) {
  3. return arr.length;
  4. }
  5. module.exports = {
  6. getArrayLength,
  7. maxInterval,
  8. };

This can be described by the following .d.ts:

  1. export function getArrayLength(arr: any[]): number;
  2. export const maxInterval: 12;

The TypeScript playground can show you the .d.ts equivalent for JavaScript code. You can try it yourself here.

The .d.ts syntax intentionally looks like ES Modules syntax. ES Modules was ratified by TC39 in 2019, while it has been available via transpilers for a long time, however if you have a JavaScript codebase using ES Modules:

  1. export function getArrayLength(arr) {
  2. return arr.length;
  3. }

This would have the following .d.ts equivalent:

  1. export function getArrayLength(arr: any[]): number;

Default Exports

In CommonJS you can export any value as the default export, for example here is a regular expression module:

  1. module.exports = /hello( world)?/;

Which can be described by the following .d.ts:

  1. const helloWorld: RegExp;
  2. export default helloWorld;

Or a number:

  1. module.exports = 3.142;
  1. const pi: number;
  2. export default pi;

One style of exporting in CommonJS is to export a function. Because a function is also an object, then extra fields can be added are included in the export.

  1. function getArrayLength(arr) {
  2. return arr.slice;
  3. }
  4. getArrayLength.maxInterval = 12;
  5. module.exports = getArrayLength;

Which can be described with:

  1. export default function getArrayLength(arr: any[]): number;
  2. export const maxInterval: 12;

Note that using export default in your .d.ts files requires esModuleInterop: true to work. If you can’t have esModuleInterop: true in your project, such as when you’re submitting a PR to Definitely Typed, you’ll have to use the export= syntax instead. This older syntax is harder to use but works everywhere. Here’s how the above example would have to be written using export=:

  1. declare function getArrayLength(arr: any[]): number;
  2. declare namespace getArrayLength {
  3. declare const maxInterval: 12;
  4. }
  5. export = getArrayLength;

See Module: Functions for details of how that works, and the Modules reference page.

Handling Many Consuming Import

There are many ways to import a module in modern consuming code:

  1. const fastify = require("fastify");
  2. const { fastify } = require("fastify");
  3. import fastify = require("fastify");
  4. import * as Fastify from "fastify";
  5. import { fastify, FastifyInstance } from "fastify";
  6. import fastify from "fastify";
  7. import fastify, { FastifyInstance } from "fastify";

Covering all of these cases requires the JavaScript code to actually support all of these patterns. To support many of these patterns, a CommonJS module would need to look something like:

  1. class FastifyInstance {}
  2. function fastify() {
  3. return new FastifyInstance();
  4. }
  5. fastify.FastifyInstance = FastifyInstance;
  6. // Allows for { fastify }
  7. fastify.fastify = fastify;
  8. // Allows for strict ES Module support
  9. fastify.default = fastify;
  10. // Sets the default export
  11. module.exports = fastify;

Types in Modules

You may want to provide a type for JavaScript code which does not exist

  1. function getArrayMetadata(arr) {
  2. return {
  3. length: getArrayLength(arr),
  4. firstObject: arr[0],
  5. };
  6. }
  7. module.exports = {
  8. getArrayMetadata,
  9. };

This can be described with:

  1. export type ArrayMetadata = {
  2. length: number;
  3. firstObject: any | undefined;
  4. };
  5. export function getArrayMetadata(arr: any[]): ArrayMetadata;

This example is a good case for using generics to provide richer type information:

  1. export type ArrayMetadata<ArrType> = {
  2. length: number;
  3. firstObject: ArrType | undefined;
  4. };
  5. export function getArrayMetadata<ArrType>(
  6. arr: ArrType[]
  7. ): ArrayMetadata<ArrType>;

Now the type of the array propagates into the ArrayMetadata type.

The types which are exported can then be re-used by consumers of the modules using either import or import type in TypeScript code or JSDoc imports.

Namespaces in Module Code

Trying to describe the runtime relationship of JavaScript code can be tricky. When the ES Module-like syntax doesn’t provide enough tools to describe the exports then you can use namespaces.

For example, you may have complex enough types to describe that you choose to namespace them inside your .d.ts:

  1. // This represents the JavaScript class which would be available at runtime
  2. export class API {
  3. constructor(baseURL: string);
  4. getInfo(opts: API.InfoRequest): API.InfoResponse;
  5. }
  6. // This namespace is merged with the API class and allows for consumers, and this file
  7. // to have types which are nested away in their own sections.
  8. declare namespace API {
  9. export interface InfoRequest {
  10. id: string;
  11. }
  12. export interface InfoResponse {
  13. width: number;
  14. height: number;
  15. }
  16. }

To understand how namespaces work in .d.ts files read the .d.ts deep dive.

Optional Global Usage

You can use export as namespace to declare that your module will be available in the global scope in UMD contexts:

  1. export as namespace moduleName;

Reference Example

To give you an idea of how all these pieces can come together, here is a reference .d.ts to start with when making a new module

  1. // Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
  2. // Project: [~THE PROJECT NAME~]
  3. // Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
  4. /*~ This is the module template file. You should rename it to index.d.ts
  5. *~ and place it in a folder with the same name as the module.
  6. *~ For example, if you were writing a file for "super-greeter", this
  7. *~ file should be 'super-greeter/index.d.ts'
  8. */
  9. /*~ If this module is a UMD module that exposes a global variable 'myLib' when
  10. *~ loaded outside a module loader environment, declare that global here.
  11. *~ Otherwise, delete this declaration.
  12. */
  13. export as namespace myLib;
  14. /*~ If this module exports functions, declare them like so.
  15. */
  16. export function myFunction(a: string): string;
  17. export function myOtherFunction(a: number): number;
  18. /*~ You can declare types that are available via importing the module */
  19. export interface SomeType {
  20. name: string;
  21. length: number;
  22. extras?: string[];
  23. }
  24. /*~ You can declare properties of the module using const, let, or var */
  25. export const myField: number;

Library file layout

The layout of your declaration files should mirror the layout of the library.

A library can consist of multiple modules, such as

  1. myLib
  2. +---- index.js
  3. +---- foo.js
  4. +---- bar
  5. +---- index.js
  6. +---- baz.js

These could be imported as

  1. var a = require("myLib");
  2. var b = require("myLib/foo");
  3. var c = require("myLib/bar");
  4. var d = require("myLib/bar/baz");

Your declaration files should thus be

  1. @types/myLib
  2. +---- index.d.ts
  3. +---- foo.d.ts
  4. +---- bar
  5. +---- index.d.ts
  6. +---- baz.d.ts

Testing your types

If you are planning on submitting these changes to DefinitelyTyped for everyone to also use, then we recommend you:

  1. Create a new folder in node_modules/@types/[libname]
  2. Create an index.d.ts in that folder, and copy the example in
  3. See where your usage of the module breaks, and start to fill out the index.d.ts
  4. When you’re happy, clone DefinitelyTyped/DefinitelyTyped and follow the instructions in the README.

Otherwise

  1. Create a new file in the root of your source tree: [libname].d.ts
  2. Add declare module "[libname]" { }
  3. Add the template inside the braces of the declare module, and see where your usage breaks