TypeScript in JavaScript Projects (JSDoc)
JSDoc comments let you add TypeScript type checking to plain JavaScript files without renaming them to .ts.
This is ideal when you want to migrate a project step by step, or when you simply want type safety in a JavaScript codebase.
Note: the examples on this page are written as JavaScript with JSDoc types. Save them as .js files with // @ts-check at the top (or checkJs enabled) to see the type checking described. In a .ts file the compiler ignores JSDoc types and relies on the regular annotations instead.
Getting Started
To make TypeScript check your JavaScript files you need to:
- Create a
tsconfig.jsonfile (if you do not have one) - Enable
checkJsin it, or put// @ts-checkat the top of the individual files you want checked
Example: JSDoc for Type Safety
A JSDoc comment above the function tells TypeScript that both parameters and the return value are numbers.
// @ts-check enabled at the top of the file.
Write an add(a, b) function documented with @param {number} a, @param {number} b and @returns {number} that returns the sum. Only the declaration is needed — the program prints nothing.
Output: TypeScript file and captured output
// @ts-check
/**
* Adds two numbers.
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
Objects and Interfaces
You can describe object shapes directly in a JSDoc tag, or give them a name with @typedef so the type can be reused in many places.
Inline Object Types
The parameter type is written inline as an object literal type. In a checked .js file, calling greet without lastName is reported as an error.
// @ts-check on, document greet(person) with @param {{ firstName: string, lastName: string, age?: number }} person and return Hello, <firstName> <lastName>. Call it once with a complete object (John Doe) and once with only firstName: "Jane", marking the second call with a comment as the missing-lastName error a checked .js file reports. Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/**
* @param {{ firstName: string, lastName: string, age?: number }} person
*/
function greet(person) {
return `Hello, ${person.firstName} ${person.lastName}`;
}
greet({ firstName: 'John', lastName: 'Doe' }); // OK
greet({ firstName: 'Jane' }); // Error: Property 'lastName' is missing
Using @typedef for Complex Types
@typedef plus @property lines define a named User type. Square brackets mark an optional property, and a parenthesised union restricts role to a few values.
@typedef and @property tags describing an object type in JSDoc.
Define a User typedef with id: number, username: string, optional email, a role limited to "admin" | "user" | "guest", and a getFullName method returning a string. Declare currentUser with /** @type {User} */ (id 1, username johndoe, role admin, getFullName() returning John Doe) and print currentUser.role.
Output: TypeScript file and captured output
// @ts-check
/**
* @typedef {Object} User
* @property {number} id - The user ID
* @property {string} username - The username
* @property {string} [email] - Optional email address
* @property {('admin'|'user'|'guest')} role - User role
* @property {() => string} getFullName - Method that returns full name
*/
/** @type {User} */
const currentUser = {
id: 1,
username: 'johndoe',
role: 'admin',
getFullName() {
return 'John Doe';
}
};
// TypeScript will provide autocomplete for User properties
console.log(currentUser.role);
Extending Types
An intersection (Point & { z: number }) builds a new type on top of an existing one. In a checked .js file the object without z is an error, which the @ts-expect-error comment expects. Compiled as a .ts file the JSDoc types are ignored, so the compiler reports the directive as unused instead.
Point as { x: number, y: number } and Point3D as Point & { z: number }. Declare point3d typed as Point3D with x 1, y 2, z 3, then a point2d object without z preceded by a // @ts-expect-error - missing z property comment. In a .js file with // @ts-check the missing z is the expected error; in a .ts file JSDoc types are ignored, so the compiler reports the directive as unused instead.
Output: TypeScript file and captured output
// @ts-check
/** @typedef {{ x: number, y: number }} Point */
/**
* @typedef {Point & { z: number }} Point3D
*/
/** @type {Point3D} */
const point3d = { x: 1, y: 2, z: 3 };
// @ts-expect-error - missing z property
const point2d = { x: 1, y: 2 };
Function Types
JSDoc can describe every part of a function: its parameters, its return value, callback types and even several overloaded signatures.
Function Declarations
Each @param gives a parameter a type and a short description; @returns does the same for the result.
// @ts-check on, write calculateArea(width, height) documented with @param {number} for both parameters (with short descriptions) and @returns {number} The calculated area, returning the product. Store calculateArea(10, 20) in area. Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/**
* Calculates the area of a rectangle
* @param {number} width - The width of the rectangle
* @param {number} height - The height of the rectangle
* @returns {number} The calculated area
*/
function calculateArea(width, height) {
return width * height;
}
// TypeScript knows the parameter and return types
const area = calculateArea(10, 20);
Function Expressions and Callbacks
@callback defines a reusable function type. It can then be used with @type on an arrow function or as a parameter type.
@callback for typing function expressions and callback parameters in JSDoc.
Define a StringProcessor callback type taking a string and returning a string. Declare toUpperCase with @type {StringProcessor} as an arrow function, then processStrings(strings, processor) documented with @param {string[]}, @param {StringProcessor} and @returns {string[]} that maps the array through the processor. Call it with ["hello", "world"] and toUpperCase. Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/**
* @callback StringProcessor
* @param {string} input
* @returns {string}
*/
/**
* @type {StringProcessor}
*/
const toUpperCase = (str) => str.toUpperCase();
/**
* @param {string[]} strings
* @param {StringProcessor} processor
* @returns {string[]}
*/
function processStrings(strings, processor) {
return strings.map(processor);
}
const result = processStrings(['hello', 'world'], toUpperCase);
// result will be ['HELLO', 'WORLD']
Function Overloads
Several @overload blocks give one function more than one signature. The last block describes the implementation that handles all of them.
@overload tags giving one function two typed signatures.
Write add(a, b) with two @overload blocks — string plus string returns string, number plus number returns number — followed by the implementation signature string | number. If either argument is a string, return the concatenation via String(), otherwise the numeric sum. Call it as add("Hello, ", "World!") and add(10, 20). Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/**
* @overload
* @param {string} a
* @param {string} b
* @returns {string}
*/
/**
* @overload
* @param {number} a
* @param {number} b
* @returns {number}
*/
/**
* @param {string | number} a
* @param {string | number} b
* @returns {string | number}
*/
function add(a, b) {
if (typeof a === 'string' || typeof b === 'string') {
return String(a) + String(b);
}
return a + b;
}
const strResult = add('Hello, ', 'World!'); // string
const numResult = add(10, 20); // number
Advanced Types
More advanced TypeScript features — unions, intersections, generics, mapped and conditional types — are also available through JSDoc.
Union and Intersection Types
& combines types and | allows one of several types. An in check narrows a union inside a function.
@typedef.
Define Person (name, age), Employee as Person & { employeeId: string } and Visitor as Person | { guestId: string, visitDate: Date }. Declare an employee (Alice, 30, E123) and a guest (G456, new Date()), then getVisitorId(visitor) that uses an "guestId" in visitor check to return either guestId or name. Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/** @typedef {{ name: string, age: number }} Person */
/** @typedef {Person & { employeeId: string }} Employee */
/** @typedef {Person | { guestId: string, visitDate: Date }} Visitor */
/** @type {Employee} */
const employee = {
name: 'Alice',
age: 30,
employeeId: 'E123'
};
/** @type {Visitor} */
const guest = {
guestId: 'G456',
visitDate: new Date()
};
/**
* @param {Visitor} visitor
* @returns {string}
*/
function getVisitorId(visitor) {
if ('guestId' in visitor) {
return visitor.guestId; // TypeScript knows this is a guest
}
return visitor.name; // TypeScript knows this is a Person
}
Mapped and Conditional Types
@template makes a typedef generic. The typedef body can then use mapped types, key remapping and conditional types just like a .ts file.
@template typedefs can express mapped and conditional types.
Define a generic MethodNames<T> typedef that keeps only the keys of T whose values are functions, and a generic Getters<T> mapped type that renames each key K to `get${Capitalize<K>}` returning () => T[K]. Declare userGetters typed as Getters<{ name: string, age: number }> with getName returning "John" and getAge returning 30, and read both into name and age. Add export {} so name does not clash with the global. Nothing is printed.
Output: TypeScript file and captured output
// @ts-check
/**
* @template T
* @typedef {{ [K in keyof T]: T[K] extends Function ? K : never }[keyof T]} MethodNames
*/
/**
* @template T
* @typedef {{
* [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
* }} Getters
*/
/** @type {Getters<{ name: string, age: number }>} */
const userGetters = {
getName: () => 'John',
getAge: () => 30
};
// TypeScript enforces the return types
const name = userGetters.getName(); // string
const age = userGetters.getAge(); // number
// Make this file a module so `name` does not clash with the global `name`
export {};
Type Imports
A JSDoc type can be imported from another file or from a package with import('...') inside a @typedef. For libraries that ship no types, you can write your own declaration file (.d.ts).
Importing types in JSDoc
Three ways to bring a type into a JavaScript file: from a local TypeScript file, from a package in node_modules, and with a new name.
// @ts-check
// Importing types from TypeScript files
/** @typedef {import('./types').User} User */
// Importing types from node_modules
/** @typedef {import('express').Request} ExpressRequest */
// Importing with renaming
/** @typedef {import('./api').default as ApiClient} ApiClient */
Declaration file for a module
A types.d.ts file that declares the my-module package: a Config interface and two exported functions.
// types.d.ts
declare module 'my-module' {
export interface Config {
apiKey: string;
timeout?: number;
retries?: number;
}
export function initialize(config: Config): void;
export function fetchData<T = any>(url: string): Promise<T>;
}
Best Practices
Follow these practices when you use JSDoc with TypeScript:
- Put
// @ts-checkat the top of every file you want checked - Use
@typedeffor complex types that appear in more than one place - Document all function parameters and return types
- Use
@templatefor generic functions and types - Write declaration files (
.d.ts) for third-party libraries without types - Prefer
@ts-expect-errorover@ts-ignorewhen you expect an error
Common Pitfalls
Watch out for these common issues:
- Missing
// @ts-check: without it (orcheckJs) nothing is checked - Incorrect JSDoc syntax: a single typo can switch off type checking for that comment
- Type conflicts: types from different sources may not match
- Inference issues: sometimes TypeScript cannot work out a type on its own
- Performance: large JavaScript files with complex types can be slow to check
Conclusion
JSDoc with TypeScript is a powerful way to add type safety to a JavaScript project without converting files to TypeScript.
It is especially useful for:
- Gradually migrating a JavaScript codebase to TypeScript
- Adding type checking to an existing JavaScript project
- Working in environments where
.tsfiles are not supported - Documenting JavaScript code with type information
With the patterns on this page you get many of TypeScript's benefits while still writing JavaScript.
Remember: for new projects or complete migrations, .ts files still give the best TypeScript experience.
Ready to start? Add // @ts-check to your JavaScript files and start adding JSDoc type annotations. The compiler will catch errors before they reach production.