Type JavaScript with JSDoc

Learn how to add type annotations to JavaScript using JSDoc comments. Cover @type, @param, @returns, @typedef, @template, and how TypeScript reads them.

7 min read

JSDoc TypeScript is a comment-based way to add type information to plain JavaScript. TypeScript reads specially formatted comments above your variables and functions, then uses them for type checking, autocompletion, and error reporting, even though the file itself is still JavaScript.

This gives a project type safety without renaming files to the TypeScript extension. You write types as structured comments above your code, and the compiler treats those comments the same way it treats a real TypeScript annotation. The examples below use the tags TypeScript recognizes most often, starting with the simplest one.

JSDoc types only take effect once TypeScript is actively checking the file, either through the checkJs compiler option in tsconfig.json or a // @ts-check comment placed at the top of an individual file. Every example on this page assumes one of those two is turned on, and the effect of skipping this step is covered later in the mistakes section.

@type: Declare a Variable's Type

The @type tag is the most direct way to type a variable in JSDoc. Write it in a block comment on the line above the variable, and TypeScript checks every later use of that variable against the declared type, the same way it checks a typed declaration inside a real TypeScript file. This works for simple primitives and for complex object or array shapes.

javascriptjavascript
// @ts-check
 
/** @type {string} */
let name = "Alice";
 
/** @type {number[]} */
let scores = [95, 87, 91];

TypeScript now treats name as a string and scores as an array of numbers for the rest of the file. Any later assignment that breaks either type produces a compiler error at the exact line where it happens, not just at the point where the variable was declared.

The next example shows what that error looks like. A string-typed variable is declared first, then reassigned to a number:

javascriptjavascript
/** @type {string} */
let title = "Hello";
 
title = 42;

Assigning a number to a variable that was declared as a string produces a compile-time error, the same as it would in a typed TypeScript file:

texttext
Type 'number' is not assignable to type 'string'.

TypeScript flags the reassignment as soon as the file is checked, even though plain JavaScript would allow it without complaint at runtime. This is exactly why checkJs or @ts-check has to be active, since without a checker reading the file, the comment above the variable is inert documentation with no enforcement behind it.

Sometimes a value arrives with an unknown shape, such as parsed JSON, and there is no way to attach a type at the point of declaration. For that case, @type can also work as a cast:

javascriptjavascript
const raw = JSON.parse('{"x": 10}');
const point = /** @type {{ x: number, y: number }} */ (raw);

The parenthesized cast tells TypeScript to treat the parsed value as the declared object shape from that point forward in the file. This only affects what the compiler believes about the value; the actual object returned at runtime is unchanged, and TypeScript does no verification that the real data matches the shape you claimed.

@param and @returns: Type Function Signatures

Function signatures are usually the most valuable place to add JSDoc types, because every caller benefits from the check. The @param tag types each parameter in order, and the @returns tag, or its shorter form @return, types the value the function sends back.

javascriptjavascript
// @ts-check
 
/**
 * @param {string} name
 * @param {number} age
 * @returns {string}
 */
function greet(name, age) {
  return `${name} is ${age} years old`;
}

With this comment block in place, TypeScript checks every call site against the declared parameter types, not just the function body itself.

javascriptjavascript
greet("Bob", 30);
greet("Bob", "thirty");

The first call passes a number where age expects one, so it compiles. The second call passes a string instead, and TypeScript reports the mismatch before the code ever runs:

texttext
Argument of type 'string' is not assignable to parameter of type 'number'.

Catching this at compile time avoids a confusing runtime string like "Bob is thirty years old" from ever reaching a user.

Not every parameter has to be required. Wrapping a parameter name in square brackets marks it optional, which matches how an optional parameter looks in a TypeScript file:

javascriptjavascript
/**
 * @param {string} name
 * @param {number} [age]
 * @returns {string}
 */
function describe(name, age) {
  if (age !== undefined) {
    return `${name} is ${age}`;
  }
  return name;
}

Because age is optional, TypeScript allows calling describe with just a name and still requires the type to be a number whenever a caller does provide it. A related syntax marks a parameter optional while also documenting its default value, written as the parameter name followed by an equals sign and the default, which is useful when the function body itself sets that default.

@typedef: Create Named Types

Repeating a long object shape in every type and parameter tag gets noisy fast. The @typedef tag solves that by creating a named, reusable type that the rest of the file can reference by name, similar to a type alias or interface in a TypeScript file.

javascriptjavascript
/**
 * @typedef {Object} User
 * @property {string} name
 * @property {number} id
 * @property {string} [email]
 */

This block does not create a variable. It only teaches TypeScript the shape called User, made of a required name and id plus an optional email. The next snippet uses that shape and shows what happens when a property name is misspelled:

javascriptjavascript
/** @type {User} */
const currentUser = {
  name: "Alice",
  id: 1,
};
 
currentUser.namme;

The misspelled property access fails to compile, and TypeScript even suggests the correct name based on the User shape declared above:

texttext
Property 'namme' does not exist on type 'User'. Did you mean 'name'?

TypeScript checks the object literal against the User shape and then flags the typo on the following line, complete with a suggested fix. This is the same accuracy a TypeScript file would give you, driven entirely by comments.

For shapes that do not need individual property descriptions, a shorter inline form works too, using the same object and function syntax TypeScript already supports:

javascriptjavascript
/** @typedef {{ x: number, y: number }} Point */
/** @typedef {string | number} ID */

The inline syntax puts the whole shape between the braces on one line. It is usually faster to write than multiple property lines, but it loses the ability to document each field individually, so it works best for small or self-explanatory shapes.

@callback: Type Function Signatures

A function that gets passed around as a value, such as an event handler, needs a named function type rather than a named object type. The @callback tag defines exactly that: a reusable function signature you can reference the same way you reference a named type.

javascriptjavascript
// @ts-check
/**
 * @callback EventHandler
 * @param {string} event
 * @param {object} data
 * @returns {void}
 */
 
/** @type {EventHandler} */
const onClick = (event, data) => {
  console.log(event, data);
};

Any variable typed as EventHandler must accept a string and an object argument and return nothing. This keeps every handler in a codebase consistent without writing the same parameter list out by hand each time, and it is equivalent to a function type alias in a TypeScript file.

@template: Generic Functions and Classes

Some functions work the same way regardless of the type they receive, such as a function that returns whatever value it was given. The @template tag declares a generic type parameter for exactly that situation, letting a single function stay accurate for every type it is called with.

javascriptjavascript
// @ts-check
/**
 * @template T
 * @param {T} value
 * @returns {T}
 */
function identity(value) {
  return value;
}
 
const a = identity("hello");
const b = identity(42);

TypeScript infers T separately at each call site instead of locking it to one fixed type. The first variable ends up typed as a string because that call passed a string, and the second ends up typed as a number because that call passed a number.

A function can declare more than one type parameter, either by separating names with a comma in one tag or by repeating the tag once per parameter:

javascriptjavascript
/**
 * @template T, U
 * @param {T} first
 * @param {U} second
 * @returns {[T, U]}
 */
function pair(first, second) {
  return [first, second];
}

Type parameters can also be constrained so that only certain types are allowed. Put the constraint in braces before the parameter name; only the first parameter in a list can carry a constraint this way:

javascriptjavascript
/**
 * @template {string} K
 * @param {K} key
 * @returns {K}
 */
function validateKey(key) {
  return key;
}

Here K is restricted to string types, so calling validateKey with a number fails to compile even though the function body never checks the type itself. A type parameter can also carry a default value, written in brackets after the parameter name, so that omitting the type argument falls back to a sensible value instead of resolving to the unknown type.

javascriptjavascript
/**
 * @template [T=string]
 * @param {T} value
 */
function process(value) { /* body omitted for brevity */ }

Object, Union, and Array Types

Beyond the tags above, JSDoc type positions accept the same type syntax TypeScript already uses everywhere else. Object shapes, unions, intersections, and arrays all read the same inside a JSDoc comment as they would inside a TypeScript file.

javascriptjavascript
/** @type {{ name: string, age: number, email?: string }} */
const user = { name: "Alice", age: 30 };
 
/** @type {string | number} */
let id = "abc123";
 
/** @type {number[]} */
let scores = [95, 87, 91];

The question mark after a property name marks it optional, the pipe between two type names forms a union, and the square brackets after a type name form an array type. None of this syntax changes based on being written in a comment instead of a TypeScript file.

Index signatures, which describe an object with unknown keys but a known value type, also work in JSDoc using either the closure-style dotted form or the TypeScript bracket form:

javascriptjavascript
/** @type {Object.<string, number>} */
const counts = { a: 1, b: 2 };
 
/** @type {{ [key: string]: boolean }} */
const flags = { debug: true, verbose: false };

Both lines describe an object where every value must match the given type, regardless of how many keys the object ends up having. The bracket form is usually easier to read for developers coming from TypeScript files, since it matches index signature syntax exactly.

Function Types

A variable that holds a function, rather than a call to one, still needs a type for its parameters and return value. Two syntaxes cover this in JSDoc, and picking one consistently keeps a codebase easier to scan.

javascriptjavascript
/** @type {(s: string, n: number) => boolean} */
const isLongerThan = (s, n) => s.length > n;
 
/** @type {function(string, number): boolean} */
const isLongerThanClosure = (s, n) => s.length > n;

The first line uses the same arrow syntax a TypeScript file would use for a function type, and it is usually the more readable choice for teams already familiar with TypeScript. The second line is the older Closure Compiler style that JSDoc tooling has supported for longer; both compile to the identical type, so a project should pick one and stay consistent.

Import Types From Other Files

Type information does not have to live in the same file as the code that uses it. A JSDoc comment can import a type from a declaration file or a TypeScript file using the same import syntax TypeScript supports inline, without adding a runtime import statement.

javascriptjavascript
// @ts-check
 
/**
 * @param {import("./types").User} user
 */
function displayUser(user) {
  console.log(user.name);
}

This keeps one shared definition of the User type in a dedicated types file, and every JavaScript file that needs it references that single source instead of redefining the shape locally. Nothing about this import exists once the code runs; it disappears entirely during compilation, exactly like every other JSDoc type in this article.

Referencing an imported path inline gets repetitive across many annotations in the same file. The dedicated @import tag solves that by bringing names into scope once, so later tags can reference them directly:

javascriptjavascript
// @ts-check
 
/** @import { User, Post } from "./types" */
 
/** @type {User} */
const author = { name: "Alice", id: 1 };
 
/** @type {Post[]} */
const posts = [];

After the @import line, both User and Post act like local type names for the rest of the file. This tag was added specifically for JSDoc files and has no equivalent runtime effect, the same as the inline import form above it.

Class Annotations

Classes written in plain JavaScript can still express visibility and inheritance intent through JSDoc, even though JavaScript itself has no compile-time access modifiers before the private class fields syntax. The private modifier is the most common one, and it works on any property assigned inside the constructor.

javascriptjavascript
// @ts-check
 
class Counter {
  constructor() {
    /** @private */
    this.value = 0;
  }
 
  increment() {
    this.value += 1;
  }
}

With the private modifier in place, TypeScript treats the value property as accessible only from inside the Counter class itself. Accessing it from outside the class, as shown below, is flagged even though nothing in plain JavaScript would stop the property access at runtime.

javascriptjavascript
const c = new Counter();
c.value;

Reading the property from outside the class compiles fine in plain JavaScript, but a checked file reports it as a violation:

texttext
Property 'value' is private and only accessible within class 'Counter'.

A handful of other class tags follow the same pattern of mirroring TypeScript keywords in comment form. The readonly modifier marks a property immutable after it is first set, the extends tag documents a generic base class the way extending a class with a type argument would in a TypeScript file, the override tag marks a method that intentionally replaces a base class method, and the implements tag declares that a class satisfies a given interface shape.

javascriptjavascript
class Animal {
  speak() { return "..."; }
}
 
/** @extends {Animal} */
class Dog extends Animal {
  /** @override */
  speak() { return "woof"; }
}

The extends tag above documents that Dog inherits from Animal for type-checking purposes, which matters most when the base class itself is generic. The override tag on the speak method lets TypeScript confirm that Animal actually has a method with that name to override, catching typos that would otherwise silently create a new method instead of replacing the intended one.

When JSDoc Makes Sense

JSDoc is a good fit in a specific set of situations rather than as a universal alternative to TypeScript files:

  • The project cannot rename files to TypeScript yet, due to build tooling, bundler configuration, or team constraints.
  • Type checking needs to be added to a codebase that must stay as JavaScript for other reasons.
  • Types are being added to a file gradually, as a step before eventually renaming it.
  • A library is being published and needs declaration file generation directly from its JavaScript source.

For most projects, JSDoc works best as a stepping stone rather than a permanent home for complex types. Every type shown on this page means exactly the same thing it would mean in TypeScript syntax; only the surrounding comment punctuation differs.

When a file is ready, converting its JSDoc comments to real TypeScript syntax and enabling the checker along the way is described in Use checkJs in TypeScript. For generating declaration files once a library's public shape is finalized, see Publish Type Declarations for a Package.

Common Mistakes

Forgetting @ts-check. JSDoc annotations have no effect on their own; TypeScript only reads them once type checking is enabled for the file. Add the comment at the top of the file, or enable checkJs globally in tsconfig.json to cover every JavaScript file at once.

Using @type {Object} instead of a specific shape. The generic Object type is a common source of confusion. It is not the same as the any type, and TypeScript still blocks access to properties it does not know about, so code that looks type-safe fails to compile the moment you read a property from it. Declare the specific shape expected, either inline or through a named type.

Mixing JSDoc types with runtime imports. A type annotation that imports from another file is compile-time only. It never adds a runtime require or import statement to the file, and nothing about it exists once the code is executed. If a value is actually needed at runtime, a separate import statement still has to be written for it.

Rune AI

Rune AI

Key Insights

  • @type declares the type of a variable or expression.
  • @param and @returns document function parameters and return types.
  • @typedef creates named reusable types.
  • @template defines generic type parameters.
  • TypeScript reads JSDoc types in .js files when checkJs or @ts-check is enabled.
  • Most TypeScript types work in JSDoc syntax, including unions, generics, and conditional types.
RunePowered by Rune AI

Frequently Asked Questions

Do JSDoc types work without checkJs?

Yes in a limited way. TypeScript always uses JSDoc annotations for inference in .ts files that import from .js files. But to see error reports inside the .js file itself, you need checkJs: true or // @ts-check.

Can JSDoc express all TypeScript types?

Most of them. You can use primitives, arrays, unions, generics, conditional types, template literal types, and utility types in JSDoc. The syntax is slightly different but the type system is the same.

Is JSDoc a replacement for .ts files?

It works for adding type information, but .ts files are cleaner for type-heavy code. JSDoc is best when you cannot or do not want to rename files yet, or when you are publishing types for JavaScript consumers.

Conclusion

JSDoc annotations give you TypeScript-level type checking inside .js files. Start with @type and @param for the most immediate benefit. Add @typedef for shared types and @template for generic functions. The type system is the same; only the syntax is different.