What ends up being written is not TypeScript syntax (which does require transpilation to run).
JSDoc is awesome for incremental adoption though, I've migrated a few codebases this way without too much pain.
Incidentally I've been doing my JS projects this way for 2-3 years and I really dig it.
- Type checking
- Transpiling to multiple formats (CJS/ESM, JSX and the like)
- Generating declaration files (.d.ts)
While there are modern alternatives for transpiling, much faster than tsc (esbuild, swc), typechecking and declaration generation are still very slow operations.
You just need to call this from somewhere like ts-morph.
> This proposal aims to enable developers to add type annotations to their JavaScript code, allowing those annotations to be checked by a type checker that is external to JavaScript. At runtime, a JavaScript engine ignores them, treating the types as comments.
The problem is handling errors. If the engine does a full analysis of every code path to ensure the types aren't violated, there's a startup penalty - this includes every time a dynamic import is used, which could interfere with the user as they interact with the page. I'm not entirely sure that such an analysis would be entirely possible- there are many ways to add layers of indirection to JavaScript code.
If the engine doesn't do a full analysis up front but instead checks every time a type is used, you end up with, at best, roughly the same performance characteristics that current JIT engines already have.
When I write them I don’t annotate types though, that’s already done by typescript, and editors like vscode even gives you type hints in a js file if it detects d.ts files in the source.
/// Top of file usually
/**
* @typedef {import('./typedefs/MyThing')} MyThing
*/
/// Later
/**
* @param {MyThing} x
*/
function(x) {
//
} /** @param { import('./foo.js').SomeType } arg */
function doThing(arg) {
// ...
}
The thing being imported can be a JS thing like an exported class, or a JSDoc type that's defined with @typedef.> The main limitation is that you don't have access to some TypeScript-specific syntax.
> * as, also known as type assertions (or casts)
You generally shouldn’t (as the article notes), but you can:
const foo = /** @type {Bar} */ ({
something: {
satisfying: 'Bar',
},
});
Note: the parentheses are necessary, they are the JS/JSDoc mechanism for type casts.This also works for `as const`, which is often better when you can use readonly types:
const foo = /** @type {const} */ ({
something: 'else',
});
// { readonly something: 'else' }
Better still, the satisfies operator also works (its JSDoc support lagged a bit though): /** @satisfies {Bar} */
const foo = …;
This will infer the type of `foo` and check it for assignability to `Bar`.> * is, also known as type predicates
This definitely works:
/**
* @param {unknown} value
* @return {value is Foo}
*/
const isFoo = (value) => …
You can also define the guard as a standalone/assignable type: /**
* @callback IsFoo
* @param {unknown} value
* @return {value is Foo}
*/
The @callback tag is ~equivalent to a type alias for a function signature.Also useful, if needed, the @template tag which is roughly equivalent to TS type parameters (generics) which you can use, for example, to assign generic type guards:
/**
* @template T
* @callback IsT
* @param {unknown} value
* @return {value is T}
*/
/** @type {IsT<Foo>} */
const isFoo = (value) => …
[Disclaimer: typed this comment on my phone from memory, apologies for any typos or mistakes!]Typically you would do jsconfig.json though, which is same as tsconfig.json but allowJs is already set. Mostly stylistic change, but some tooling might benefit for having jsconfig instead of tsconfig.
Additionally, I don't know about other editors but I know that VS Code subtly adjusts some workspace detail defaults based on the presence of jsconfig.json over tsconfig.json.
It's slightly more than just a stylistic change, but yeah there's no "wrong answer" if you prefer tsconfig.json to jsconfig.json, especially if you think there may be a subset of files you wind up preferring TS syntax and want to transpile/type-strip in the future (dropping the "noEmit"). (If for some reason you need to do a bunch of generics, for instance, that's a lot easier with type-stripping from TS syntax than trying to squeeze in JSDoc. Also, maybe you'll bump into a situation where TS downleveling is helpful, for a while I had projects that needed downlevelIteration to support certain IE/Safari versions and letting TS downlevel that was a lot easier and cleaner than the Babel-based alternatives.)
Someone here just suggested to import type definition files with JS doc annotations, and that's actually a good idea.
It's like those recipe blogs that have to give you their life story before telling you how to make a meatball. Just get to the damn point and skip the bullshit, please.