Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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/**/*"
  ]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.