Recipe · Objects and data

How do I write an enum in JavaScript with JSDoc?

  • Node.js (ES modules)
  • Enums
  • @typedef
  • Object.freeze
  • @enum
  • Checked with TypeScript 5.9

The problem In TypeScript I would write enum OrderStatus. My code is plain JavaScript. How do I get a fixed set of named values that tsc checks, and what is wrong with the @enum tag?

The short answer

Keep the values in a frozen object, and name the union of its values with a @typedef:

status.js status.ts TypeScript 5.9.3 · checked 21 October 2026
export const OrderStatus = Object.freeze({
  Draft: "draft",
  Paid: "paid",
  Shipped: "shipped",
});

/** @typedef {(typeof OrderStatus)[keyof typeof OrderStatus]} OrderStatusValue */

/**
 * @param {{ id: string, status: OrderStatusValue }} order
 * @returns {boolean}
 */
export function canShip(order) {
  return order.status === OrderStatus.Paid;
}

canShip({ id: "A-1001", status: OrderStatus.Paid });
canShip({ id: "A-1002", status: "draft" });
export const OrderStatus = Object.freeze({
  Draft: "draft",
  Paid: "paid",
  Shipped: "shipped",
});

export type OrderStatusValue = (typeof OrderStatus)[keyof typeof OrderStatus];

export function canShip(order: { id: string; status: OrderStatusValue }): boolean {
  return order.status === OrderStatus.Paid;
}

canShip({ id: "A-1001", status: OrderStatus.Paid });
canShip({ id: "A-1002", status: "draft" });
A frozen object for the values, a @typedef for the union.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Code can use OrderStatus.Paid or the plain string "draft"; both are members of OrderStatusValue, and anything else is an error.

Why it works

Object.freeze returns a read-only version of the object, and TypeScript keeps each value’s literal type ("draft", not string) because a frozen property can never change. typeof OrderStatus is the object’s type, keyof typeof OrderStatus is "Draft" | "Paid" | "Shipped", and indexing the object type with its keys gives the values: "draft" | "paid" | "shipped". The TypeScript handbook’s page on enums suggests the same pattern, an object with as const, for code that wants plain objects.

The same thing in TypeScript

The TypeScript equivalent tab is the same code with type OrderStatusValue = ... in place of the @typedef. A TypeScript enum would also create the object, but it is TypeScript-only syntax: in a .js file it is an error (TS8006).

What goes wrong

A misspelled status is caught at the call (TS2345), and the frozen object refuses changes (TS2540):

status.js Error confirmed · TypeScript 5.9.3 · checked 21 October 2026
export const OrderStatus = Object.freeze({
  Draft: "draft",
  Paid: "paid",
  Shipped: "shipped",
});

/** @typedef {(typeof OrderStatus)[keyof typeof OrderStatus]} OrderStatusValue */

/** @param {OrderStatusValue} status */
export function setStatus(status) {
  return status;
}

setStatus("shiped");error TS2345: Argument of type '"shiped"' is not assignable to parameter of type 'OrderStatusValue'.
OrderStatus.Paid = "settled";error TS2540: Cannot assign to 'Paid' because it is a read-only property.
A value outside the union, and an attempt to change a member.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

The @enum tag looks like the obvious tool, but @enum {string} types each member as string, so a misspelled status passes, and only unrelated types are caught:

status.js Error confirmed · TypeScript 5.9.3 · checked 21 October 2026
/** @enum {string} */
export const OrderStatus = {
  Draft: "draft",
  Paid: "paid",
  Shipped: "shipped",
};

/** @param {OrderStatus} status */
export function setStatus(status) {
  return status;
}

setStatus("shiped");
/** @type {number} */
const count = setStatus(OrderStatus.Paid);error TS2322: Type 'string' is not assignable to type 'number'.
@enum {string} accepts any string.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • /** @type {const} */ works too. A const cast on the object literal keeps the literals and makes the properties read-only to tsc; Object.freeze also makes them read-only at runtime.
  • TypeScript 7.0 changes @enum. Its JavaScript support reads @enum differently, and the 7.0 page recommends this object pattern instead.
  • Freezing is shallow. Nested objects inside a frozen object can still change; freeze them too, or keep enum objects flat.

Questions

Can the @typedef have the same name as the object?

It can, since types and values live in separate namespaces, but a different name (OrderStatusValue) keeps it clear which one a line refers to, in JSDoc and in hovers.

How do I list every status at runtime, for a select menu?

Object.values(OrderStatus) returns the values, typed as an array of the union. That is something a union @typedef alone cannot do, and the reason to keep the object.

Does this work with numbers?

Yes. Object.freeze({ Low: 1, High: 2 }) keeps the number literals, and the @typedef becomes 1 | 2.

Sources

Last reviewed 21 October 2026.