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.
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
);
}
- There is more boilerplate because you basically have to define the enum 3 times: the union
type
, theinterface
, and the runtimeconst
. - It is more error-prone because the compiler won't check that the union
type
and theinterface
are "in sync" (but you'll probably get an error when you try to use yourconst
). - 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.)
// tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"noEmit": true,
"strict": true,
"target": "es6"
},
"include": [
"app/**/*",
"types/**/*"
]
}
In the mean time, not sure how this compares (too tired rn):
In a
.d.ts
file I declare the enum (convention addE
at end of the name), and a type that is the literal values of that enum:I declare that enum const in some other file (eg. '@/data/runes.js'):
Can use it like so (vscode correctly auto-completes
RuneTierE.
):In the consumer files I import the enum
When I hover
RuneTier
param in VSC it shows the literal values :type RuneTier = 1 | 2 | 3
I guess a small iteration over your implmentation by using the tip in above articles to create the union of the enum's values with
type Values<T> = T[keyof T]