Skip to content

Instantly share code, notes, and snippets.

@nicolashery
Last active August 9, 2024 16:06
Show Gist options
  • Save nicolashery/b30d0464dbd016aa3978129652aa1385 to your computer and use it in GitHub Desktop.
Save nicolashery/b30d0464dbd016aa3978129652aa1385 to your computer and use it in GitHub Desktop.
Emulating "enums" in JSDoc version of TypeScript

Emulating "enums" in JSDoc version of TypeScript

Problem

TypeScript has support for type-checking plain JavaScript files, which is very useful if you have an existing JS codebase and you want to test the waters and gradually add types.

There are some limitations in what you can do in JSDoc, but a lot of them can be worked-around by using type-definition files .d.ts (for example in a types/ directory). These files don't generate any JavaScript code, they are just there to provide extra type definitions to the compiler.

One thing you can't do in those .d.ts files though, is use enums. You could define them of course, but you won't get the runtime representation since the files don't generate JS code.

Solution

The solution I found requires a bit more boilerplate and is more error-prone than the pure TypeScript version, but it seems to work.

Instead of defining an enum in your type definition file, you define both a union type and an interface:

// types/models.d.ts

declare namespace Models {
  type ProductTag = "popular" | "featured" | "sale";
  interface ProductTagEnum {
    Popular: "popular";
    Featured: "featured";
    Sale: "sale";
  }

  interface Product {
    id: string;
    name: string;
    tags: Array<ProductTag>;
  }
}

Then you create a runtime representation of your "enum" using the interface. You can use this representation elsewhere in your code.

// app/models/product.js
// @ts-check

/** @type {Models.ProductTagEnum} */
const ProductTag = {
  Popular: "popular",
  Featured: "featured",
  Sale: "sale"
};

/**
 * @param {Models.Product} product
 * @returns {boolean}
 */
function isPromoted(product) {
  return (
    product.tags.indexOf(ProductTag.Featured) >= 0 &&
    product.tags.indexOf(ProductTag.Sale) >= 0
  );
}

Caveats

  • There is more boilerplate because you basically have to define the enum 3 times: the union type, the interface, and the runtime const.
  • It is more error-prone because the compiler won't check that the union type and the interface are "in sync" (but you'll probably get an error when you try to use your const).
  • You don't have to define the runtime const, you could just use a string directly (ex: product.tags.indexOf("featured")), but it makes it harder to track down where you are using your enum values (ex: can't use your IDE's "find usages" feature, need to search for the string which could show up in comments etc.)

Appendix

// tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "jsx": "react",
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmit": true,
    "strict": true,
    "target": "es6"
  },
  "include": [
    "app/**/*",
    "types/**/*"
  ]
}
@IntusFacultas
Copy link

Sadly we can't do const assertions in JSDoc

https://fettblog.eu/tidy-typescript-avoid-enums/

Have you found something more elegant since?

Very late to the party but wanted to leave this comment for future folks who land here.

You can perform as const in JSDoc, it's just a little ugly. Here's how you would do it.

export const USER_ROLES = /** @type {const} */ ({
    UNAUTHENTICATED: 'unauthenticated-user',
    AUTHENTICATED: 'authenticated-user',
});

That will result in the same behavior as

export const USER_ROLES = {
    UNAUTHENTICATED: 'unauthenticated-user',
    AUTHENTICATED: 'authenticated-user',
} as const;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment