Quick start · project shape

Type-checked JavaScript for a library that ships its types

A package written in JavaScript can give its users full types without any TypeScript source. You write JSDoc as usual; a second config tells tsc to write .d.ts files from it; package.json points TypeScript at them. Users get completions and checking in their editor, whether their own code is TypeScript or checked JavaScript.

  • Library (ships .d.ts)
  • 1 dev dependency
  • npm run lint

The source, the config that writes the declarations, and the .d.ts file shown below are one project. When this site is built, the source is checked and the declarations are written again in memory with TypeScript 5.9; the build fails if they no longer match the file on this page.

Install

A library's own tooling needs only TypeScript. Add @types/node (and "node" in types) if the library uses Node.js APIs.

Terminal
npm install --save-dev typescript

jsconfig.json

This config checks; it never writes. TypeScript's handbook suggests module: "node18" for libraries: code that works in Node.js almost always works in bundlers too, and the setting stays put across TypeScript releases. Set target to the oldest JavaScript version you support, since it decides which built-in methods the checker lets you use.

jsconfig.json
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "node18",
    "target": "es2022",
    "types": [],
    "skipLibCheck": true
  },
  "include": ["src/**/*.js"]
}

The config that writes the .d.ts files

It extends jsconfig.json and switches writing on for declarations only. "noEmit": false is required: a config that extends a jsconfig.json inherits its noEmit: true, and tsc would then write nothing, without an error. See declaration for each option.

jsconfig.types.json
{
  "extends": "./jsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "declaration": true,
    "emitDeclarationOnly": true,
    "declarationDir": "types"
  }
}

package.json

exports lists the types condition first, then default; TypeScript's 4.7 release notes say the types condition should come first. files publishes the source and the declarations. prepack runs npm run types before npm pack and npm publish, so the .d.ts files are always fresh and can stay out of git.

package.json
{
  "name": "tiny-slug",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "types": "./types/index.d.ts",
      "default": "./src/index.js"
    }
  },
  "files": ["src", "types"],
  "scripts": {
    "lint": "tsc -p jsconfig.json",
    "lint:watch": "tsc -p jsconfig.json --watch --preserveWatchOutput",
    "types": "tsc -p jsconfig.types.json",
    "prepack": "npm run types"
  }
}

What users of the package get

This is the file npm run types writes from src/index.js. The @typedef became an exported type, the optional parameter stayed optional, and the JSDoc descriptions came along for hovers.

types/index.d.ts Written by TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. Writes the .d.ts files shown. 6.0.3 : differs, see why TypeScript 6.0.3, run 21 October 2026: differs from 5.9.3. error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs from 5.9.3. jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. checked 21 October 2026
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */
/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export function slugify(title: string, options?: SlugOptions): string;
export type SlugOptions = {
    /**
     * Put between words. Default "-".
     */
    separator?: string | undefined;
    /**
     * Cut the slug to this many characters.
     */
    maxLength?: number | undefined;
};
types/index.d.ts, as tsc writes it from the JavaScript.
jsconfig.types.json, the config that writes it
{
  "extends": "./jsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "declaration": true,
    "emitDeclarationOnly": true,
    "declarationDir": "types"
  }
}

Users who still write CommonJS

The package is ES modules only. Node.js lets CommonJS code require() it without a flag from 20.19 and 22.12, as long as it has no top-level await, and TypeScript 5.8 and later model that under module: "nodenext". Supporting older Node.js from CommonJS would need a second, CommonJS build, which is beyond a no-build setup.

The example project

One module with an options object: the kind of API where JSDoc types help users most.

src/index.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. Writes the .d.ts files shown. 6.0.3 : differs, see why TypeScript 6.0.3, run 21 October 2026: differs from 5.9.3. error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs from 5.9.3. jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. checked 21 October 2026
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */

/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export function slugify(title, options = {}) {
  const { separator = "-", maxLength = Infinity } = options;
  return title
    .toLowerCase()
    .normalize("NFKD")
    .replace(/[^a-z0-9]+/g, separator)
    .replace(new RegExp(`^${separator}|${separator}$`, "g"), "")
    .slice(0, maxLength);
}
The package source: plain JavaScript with JSDoc.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "node18",
    "target": "es2022",
    "types": [],
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.js"
  ]
}

Under TypeScript 6.0 and 7.0

Every example on this page also runs under TypeScript 6.0.3 and 7.0.2 when this site is built. One of them reports something different from 5.9.3, the version the page describes; the ✗ on its badge links to its entry below.

rootDir defaults to the config's folder

TypeScript 6.0 and 7.0 · defaults that changed in 6.0

The pattern: A package that writes .d.ts files from src/ with declarationDir and no rootDir.

Why: Only the 6.0 default for rootDir causes this. 5.9 took the folder that holds every source file (src/), so src/price.js became types/price.d.ts. From 6.0 the default is the folder of the config, so the same file becomes types/src/price.d.ts, and tsc reports TS5011 asking for an explicit rootDir. The .d.ts that 6.0.3 writes is otherwise the same file. Source: TypeScript 6.0 release notes: rootDir now defaults to ..

The package source: plain JavaScript with JSDoc src/index.js

TypeScript 5.9.3

No errors.

Writes the .d.ts files the example shows.

TypeScript 6.0.3differs from 5.9.3
error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/index.d.ts
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */
/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export function slugify(title: string, options?: SlugOptions): string;
export type SlugOptions = {
    /**
     * Put between words. Default "-".
     */
    separator?: string | undefined;
    /**
     * Cut the slug to this many characters.
     */
    maxLength?: number | undefined;
};
TypeScript 7.0.2differs from 5.9.3
jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/index.d.ts
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */
export type SlugOptions = {
    /**
     * Put between words. Default "-".
     */
    separator?: string;
    /**
     * Cut the slug to this many characters.
     */
    maxLength?: number;
};
/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export declare function slugify(title: string, options?: SlugOptions): string;

What works in every version: Set "rootDir": "src" in jsconfig.types.json, which gives the 5.9 layout in every version.

7.0 writes different .d.ts files from JavaScript

TypeScript 7.0 · the .d.ts files written from JavaScript

The pattern: A package that writes its .d.ts files from JSDoc with declaration and emitDeclarationOnly.

Why: The port rewrote declaration output for JavaScript, and its list of changes says matching the old output exactly is not a goal. With rootDir set to src, TypeScript 7.0.2 still writes different files from 5.9's: a @typedef becomes an export type where its comment is (5.9 puts it last), functions and classes are export declare, an exported object is a const rather than a namespace, and optional properties lose their | undefined. Source: CHANGES.md: Declaration Emit.

The package source: plain JavaScript with JSDoc src/index.js

TypeScript 5.9.3

No errors.

Writes the .d.ts files the example shows.

TypeScript 6.0.3differs from 5.9.3
error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/index.d.ts
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */
/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export function slugify(title: string, options?: SlugOptions): string;
export type SlugOptions = {
    /**
     * Put between words. Default "-".
     */
    separator?: string | undefined;
    /**
     * Cut the slug to this many characters.
     */
    maxLength?: number | undefined;
};
TypeScript 7.0.2differs from 5.9.3
jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/index.d.ts
/**
 * @typedef {object} SlugOptions
 * @property {string} [separator] Put between words. Default "-".
 * @property {number} [maxLength] Cut the slug to this many characters.
 */
export type SlugOptions = {
    /**
     * Put between words. Default "-".
     */
    separator?: string;
    /**
     * Cut the slug to this many characters.
     */
    maxLength?: number;
};
/**
 * Turn a title into a URL slug.
 * @param {string} title
 * @param {SlugOptions} [options]
 * @returns {string}
 */
export declare function slugify(title: string, options?: SlugOptions): string;

What works in every version: Compare the .d.ts files before releasing with 7.0, and commit the new ones once they read right.

In your editor

VS Code reads this jsconfig.json with no further setup. To make it use the same TypeScript as npm run lint, see VS Code.

Editors use jsconfig.json, so while you work they check your source and never write files. Only npm run types writes the .d.ts files.

Sources

Last reviewed 20 October 2026.