JSDoc Type Safety

A Modern Framework for Maintainable Code

An interactive story exploring a modern approach to type-safe JavaScript without transpilation.

Core Philosophy

Ship Standard JS

The goal isn't to avoid TypeScript, but to ship standard, transpilation-free JavaScript. We use JSDoc and TSC as a powerful static analysis layer, not a compiler.

The "Why"

The Library-First Rationale

  • Inspired by projects like Svelte and Fastify, this approach excels for libraries.
  • It eliminates the mandatory build step, leading to a faster, lower-friction developer experience.
For applications, which will have a build step anyway, the benefits are less pronounced. For libraries, I strongly urge you to use JSDoc instead. — Rich Harris, Creator of Svelte
Core Concept

The Hybrid Imperative

A pragmatic, two-tiered system for type definitions that balances co-location with maintainability.

Tier 1

Local & Ephemeral Types

For types specific to a single function's implementation and not reused elsewhere, use inline JSDoc.

/** * @param {{ name: string, id: number }} user - The user object. * @returns {string} A greeting for the user. */ function greet(user) { return `Hello, ${user.name}!`; }
Tier 2

Shared & Public Types

For complex objects and shared data structures, define them in a dedicated -types.d.ts file. This creates a canonical, single source of truth.

// In user-types.d.ts export type User = { name: string; id: number; email?: string; }; // In main.js /** * @param {import('./user-types.d.ts').User} user * @returns {string} */ function getEmail(user) { return user.email || 'No email provided'; }
Going Further

Advanced Techniques

Let's explore some powerful JSDoc patterns that unlock the full potential of TypeScript's type inference engine.

The .d.ts Escape Hatch

Syntax in Declaration Files

Some TypeScript features, like interface or enum, are syntax-only and cannot be used in JSDoc. This is a feature, not a bug! It encourages defining complex types in .d.ts files, cleanly separating type definitions from runtime logic.

// In user-types.d.ts export interface User { id: number; name: string; } export enum Role { Admin = 'ADMIN', User = 'USER' }
Advanced Tip

Type Predicates

Use the @returns {x is Type} syntax to create type guard functions that narrow types within conditional blocks.

/** * @param {unknown} value * @returns {value is string} */ function isString(value) { return typeof value === 'string'; } function example(input) { if (isString(input)) { // TS now knows `input` is a string here console.log(input.toUpperCase()); } }
Advanced Tip

Assertion Functions

Use @returns {asserts x is Type} to create functions that throw an error if a type assumption is wrong, narrowing the type for all subsequent code.

/** * @param {unknown} value * @returns {asserts value is string} */ function assertIsString(value) { if (typeof value !== 'string') { throw new Error('Not a string!'); } } function example(input) { assertIsString(input); // TS now knows `input` is a string here console.log(input.toUpperCase()); }
Advanced Tip

Const Assertions

Apply /** @type {const} */ to an object or array literal to treat it as deeply readonly, inferring the most specific types possible.

const config = /** @type {const} */ ({ env: 'production', retries: 3, }); // config.env is now type 'production', not string // All properties are readonly. // config.retries = 4; // TypeError!
Advanced Tip

Satisfies Operator

Use /** @satisfies {Type} */ to validate that an expression conforms to a type without changing the expression's own, more specific type.

/** @typedef {'light' | 'dark'} Theme */ const myTheme = /** @satisfies {Theme} */ ('dark'); // `myTheme` has the specific type 'dark' const palette = /** @satisfies {Record} */ ({ light: ['#fff', '#eee'], dark: ['#000', '#111'], }); // `palette.dark` is known to exist
Getting Started

Project Setup

Let's look at the essential configuration files needed to enable this workflow for a new library or application.

Project Setup

package.json

Define scripts for cleaning, building declarations, and running multiple checks in parallel.

{ "scripts": { "build:0": "run-s clean", "build:1-declaration": "tsc -p declaration.tsconfig.json", "build": "run-s build:*", "check:tsc": "tsc", "check:type-coverage": "type-coverage --detail --strict", "check": "run-s clean && run-p check:*", "clean:declarations-top": "rm -rf $(find . -maxdepth 1 -type f -name '*.d.ts*' ! -name 'index.d.ts')", "clean:declarations-lib": "rm -rf $(find lib -type f -name '*.d.ts*' ! -name '*-types.d.ts')", "clean": "run-p clean:*" }, "devDependencies": { "npm-run-all2": "^8.0.1", "type-coverage": "^2.29.7", "typescript": "~5.8.3" } }
Project Setup

tsconfig.json

This file activates TypeScript's checking engine for your JavaScript project. Here are the key options:

  • "strict": true enables all of TypeScript's strict type-checking options. This is essential for catching errors.
  • "checkJs": true tells the compiler to process and report errors in .js files.
  • "noEmit": true ensures that running tsc only performs checks and does not create any output files.
  • "moduleResolution": "node16" ensures TypeScript resolves modules the same way older versions of Node.js do.
Project Setup

declaration.tsconfig.json

A separate config for publishing. It inherits from the main config and emits .d.ts files. (For libraries only)

{ "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, "noEmit": false } }
Project Setup

.gitignore

Ignore generated declaration files, but make an exception for hand-authored type definitions (like *-types.d.ts).

# Generated types *.d.ts *.d.ts.map # But NOT our hand-authored type definitions !lib/**/*-types.d.ts !index.d.ts
Ecosystem

Division of Labor

For a harmonious workflow, it's crucial to assign clear roles to our tools. TypeScript handles type correctness, while linters manage style and documentation quality.

Comparative Analysis

Rule Configuration Balance

Balancing the roles of tsc and ESLint for a harmonious workflow.

Type Correctness & Logic
Handled 100% by tsc.
Documentation Presence & Quality
Handled 90% by ESLint, 10% by manual review.
Code & JSDoc Style Consistency
Handled 100% by ESLint & Prettier.

A Unified Vision

By combining a clear philosophy, strategic tooling, and advanced JSDoc patterns, we can create JavaScript ecosystems that are robust, maintainable, and a joy to work in—all without leaving the world of standard JavaScript.

Thank You.