- The types exist whether you write them down or not.
- If they're not written down, they're written down in your head.
- Your head is very volatile and hard for others to access.
- Typing is an incredibly good form of documentation.
- JSDoc and TypeScript are standards/formats for typing. Like any tools, they both have advantages and disadvantages. Neither is objectively better than the other.
- Make informed decisions on how you'll describe your types, and then be consistent and unsurprising.
- A type checker is the computer saying, "okay then, prove it" about your program's type validity.
- Not every program benefits from the same amount of "prove it."
- Too much can be as bad as too little. You're wasting resources proving throwaway code.
- I like languages that let you decide how much you need to "prove it."
One of the lessons you learn while doing this job is that "others" includes "yourself in the future".
(Of course people will tell you this way before you find out yourself, but what do they know...)
Rust is known for being very "prove it," as you put it, but I think that it is not, and it exposes a weakness in your perspective here. In particular, Rust lets you be lax about types (Any) or other proved constraints (borrow checker bypass by unsafe, Arc, or cloning), but it forces you to decide how the unproven constraints are handled (ranging from undefined behavior to doing what you probably want with performance trade-offs). A langauge that simply lets you not prove it still must choose one of these approaches to run, but you will be less aware of what is chosen and unable to pick the right one for your use case. Writing something with, for example, Arc, .clone(), or Any is almost as easy as writing it in something like Python at the start (just arbitrarily pick one approach and go with it), but you get the aforementioned advantages and it scales better (the reader can instantly see (instead of dredging through the code to try to figure it out) "oh, this could be any type" or "oh, this is taken by ownership, so no spooky action at a distance is likely").
The same is true at multiple levels. `.clone()` is relatively easy to use, although once you learn the basic rules for referencing, that also becomes easier. `Arc` solves a specific problem you run into at a certain point sharing data between threads, but if you're not sharing data between threads (and most of the time you're not), it's just boilerplate and confusing, so you might avoid it and at worst use `Rc`. `Any` is rarely an obvious choice for most contexts, you really are going to only use it when you need it.
The result is that for most simple cases, the precise and "proven" option is typically the easiest to go for. When you deal with more complicated things, the more complicated tools are available to you. That seems exactly what the previous poster described, where you can decide yourself how much you need to prove a given thing.
Agreed. Just to clarify, my intentions with this post weren't to advocate for one over the other. Just to point out that they are the same thing. They are both TypeScript.
"Like any tools, they both have advantages and disadvantages"
You cannot make any argument based on such a position. Putting aside anyone's views on TS or JSDoc, tooling is of extremely variable quality, and lots of tools ARE objectively much worse than other tools.
It we can't point at tools and say this one is better than that one, we might as well give up.
I always remember a scene in Will & Grace where Will is trying to get his boss to say one thing is better than the other. He's brought a lovingly hand-crafted sandwich made with amazing, bread, fillings, etc. and a store bought sandwich made with cheap bread/fillings. He asks his boss to try both and say which one he likes more.
His boss says something like the store bought one reminds him of his grandma's sandwiches, so invokes nostalgia, and still can't make a decision.
Don't be that boss.
Or is you problem with the word any instead of many?
I take issue with this position because this seems to imply "PureScript and JavaScript are both JavaScript" is a true statement merely because one of them turns into the other with tooling.
TS does have some minor things like enums that need to be transformed and are actual code, but those are very few, and leftovers from early days of TS, and the TS authors regret having implemented them. For many years now the TS philosophy has been that the CODE part of TS is 100% ECMAscript, and only annotations, which are not code, are added.
The initial Babel transpiler for TS => JS, and still the most part of the current one, simply removes annotations.
It is recommended not to use the few parts that are actual code and not standard JS. They are certainly not needed any more since ES6.
People may get confused because the type syntax itself is almost like a programming language, with conditions and all. But none of that ends up as code, it's not used at runtime.
One of the IMHO worst design decisions of TS was to bundle type checking and transpiling into one tool. That caused sooo many misunderstandings and confusion.
TS from JSDoc requires a generative pass to. This is a (expected) level of indirection. (unless some tooling does it automatically)
That's easy to say when we're talking about primitive arguments in private functions, or primitive local variables, but let's not ignore the fact that it takes much more work to write a C# program than a Ruby program for instance.
We can see that by looking at a vanillajs library's typescript typings that were created after the js library when typescript didn't exist. The types are insanely complex and if you get one type wrong you can break compilation of some library user's program (its happened to me).
That being said I'm aware that dynamic programming languages are a "use at your own risk" type of language.
my take is if you treat your program as a series of data flows - then use primitives such as maps | arrays - then you don't need as much typing or typing at all. a map doesn't need to take a shape or a Person | Manager - either the keys exist or they don't and almost every language has guards to ensure you can safely navigate existence of keys.
but then again my realm is mostly around - web | data systems - if I was dealing with OS level systems and needed to make sure i have i64 ints then yeah typing would be crucial.
I don't get it. Types are a way to write code. Nothing to do with how fast/much the code changes.
This is true, but with a program built with a dynamic language, taking advantage of the fact that it's written in a dynamic language, doesn't need to make those changes at all.
I'm fan of static typing in many situations, but it's hard to deny it doesn't lead to more changes as you need to properly propagate changes.
If I change the shape of some data (such as renaming object properties), I'll need to update all the code that used that data, regardless of the type system. Static typing just ensures that I catch those cases at compile time, not runtime.
JavaScript:
// rename host -> hostname, update ONE place
function connect(opts) {
const host = opts.hostname ?? opts.host; // compat shim
return `tcp://${host}:${opts.port}`;
}
// old call sites keep working
connect({ host: "db", port: 5432 });
connect({ host: "cache", port: 6379 });
// new call sites also work
connect({ hostname: "db", port: 5432 });
TypeScript: // same compat goal, but types force propagation unless you widen them
type Opts = { port: number } & ({ host: string } | { hostname: string });
function connect(opts: Opts) {
const host = "hostname" in opts ? opts.hostname : opts.host;
return `tcp://${host}:${opts.port}`;
}
// If instead you "just rename" the type to {hostname; port},
// EVERY call site using {host; port} becomes a compile error.
Again, this is just a simple example. But multiply 100x + way messier codebases where everything are static types and intrinsically linked with each other, and every change becomes "change -> compile and see next spot to change -> change" until you've worked through 10s of files, instead of just changing it in one place.Personally, I prefer to spend the extra time I get from dynamic languages to write proper unit tests that can actually ensure the absence of specific logic bugs, rather than further ossifying the architecture with static types while changes are still ongoing.
> Personally, I prefer to spend the extra time I get from dynamic languages to write proper unit tests that can actually ensure the absence of specific logic bugs, rather than further ossifying the architecture with static types while changes are still ongoing.
I'd argue static typing makes this much easier, because I know any input types (or output types from other components) will be enforced by the type system. So I don't need to bother writing tests for "what if this parameter isn't set" or "what if this function returns something unexpected". The type system handles all of that, which eliminated a lot of tedious boilerplate tests.
Yeah, sure, and with LLMs you can do this, and you can do that. But if we're talking about languages and their features, relying on IDE features feels slightly off-topic.
But regardless, changes is changes, no matter if you, your IDE or your LLM made them. So even if your IDE makes the changes, it seems at least you can now agree that there are more changes needed, it's just that with TypeScript you have a editor who can help you refactor, and with JavaScript you haven't yet found an editor that can do so.
> So I don't need to bother writing tests for "what if this parameter isn't set" or "what if this function returns something unexpected".
Yeah, those unit tests does nothing, and people who write in dynamic languages don't write tests like that either. You test actual logic, in unit tests, and you rely on the signals that gives you.
In fact, I could bet you that if you and me both sat down and wrote the exact same application, one in JS and one in TS, we'd end up with more or less the same amount of unit tests, yet the JS codebase will be a lot more flexible once product requirements start to change.
But again, YMMV and all that, it's a highly personal preference. I don't think there is a ground truth here, different minds seem to prefer different things. I mostly work in environments where the requirements can change from day to day, and being able to adopt to those without introducing new issues is the most important thing for me, so with JS I stay.
Refacoring tools (such as renaming properties) have been supported by IDEs for decades. And in Typescript specifically, the language is designed with these tools in mind, which are developed and distributed directly by the Typescript team. For all intents and purposes, IDE integration using the Typescript language server is a feature of Typescript.
And if somehow these tools don't work, the compiler will catch it immediately! This means I can refactor with confidence, knowing any type issues will be caught automatically.
It seems like you're vastly overestimating the time and effort it takes to change types in Typescript. In my experience it's something that takes basically no time and effort, and has never caused me any issues or headaches.
I'm not saying it's hard to change types in TypeScript, I understand that your IDE is connected with the language and they're used together, but again my argument was that having types leads to having to change things in more places. Which is true too, and it's a good thing, it's on purpose, and the tools you use help with that, so yay for that! But it's still more changes, and at least for me it's important to be honest about the tradeoffs our choices leads us to.
As I mentioned earlier, I'm fan of static typing in many situations, just not all of them. And I'm not blind to the negatives they can bring too, I guess I just see more nuance in the static typing vs dynamic debate.
No, because if a piece of data is pushed through multiple layers you can just change its type at the source and the destination and not in all the layers the data is pushed through. And you can still be correct.
Imagine you have a thing called target which is a description of some endpoint. You can start with just a string, but at one point decide that instead of string you'd prefer object of a class. In dynamic language you just change the place where it originates and the place where it's used. You don't need to change any spot in 3 layers that just forearded target because they were never forced assumed it's a string.
You can achieve that in staticly typed language if you never use primitive types in your parametrs and return types or if you heavily use generics on everything, but it's not how most people write code.
Tools can help you with the changes, but such refactors aren't usually available in free tools. At least they weren't before LLMs. So the best they could do for most people was to take them on a journey through 3 layers to have them make manual change from string to Target at every spot.
fn split(str: string) => string[]
And you called it withconst first_el = split(target)[0]
If you decide `target` to suddenly be an object, then the code you wrote is incorrect, because semantically, `split` only makes sense when the argument is a string. Which is why, even with dynamic typing, you'll see that the documentation of a function will state what kind of parameters it expects.
If you a call chain like `fn1 | fn2 | fn3 | fn4 | split`, Then yeah you need to ensure what reaches `split` is in fact a string. If you're against updating fn[1-4]'s signature, let's say that it's harder to find which function needs to change in a dynamic typing systems.
Dynamic typing is useful for small programs and scripts because they are easy to iterate upon. But for longer programs, I'll take static typing anytime.
I love that too. Are there any other languages than TS that have this as a core design feature?
I can't just write C++ like:
any a = 1;
a = "Hi!";
I also can't tell JS this shouldn't be allowed. But I can tell this to TS, at any stage of evolution of my program.Modern HTML/CSS with Web Components and JSDoc is underrated. Not for everyone but should be more in the running for a modern frontend stack than it is.
Do you have anything specific in mind?
Any kind of downleveling, though that's less important these days most users only need polyfills, new syntax features like `using` are not widely used.
Minification, and bundling for web is still somewhat necessary. ESM is still tricky to use without assistance.
None of these are necessary. But if you use any of them you've already committed to having a build step, so adding in a typescript-erasure step isn't much extra work.
No Lit Element or Lit or whatever it's branded now, no framework just vanilla web components, lit-html in a render() method, class properties for reactivity, JSDoc for opt-in typing, using it where it makes sense but not junking up the code base where it's not needed...
No build step, no bundles, most things stay in light dom, so just normal CSS, no source maps, transpiling or wasted hours with framework version churn...
Such a wonderful and relaxing way to do modern web development.
Seriously, start a project and use only the standards. You'll be surprised how good the experience can be.
Try my tiny web components lib if you want to keep JSX but not the rest of React: https://github.com/webjsx/magic-loop
- signals, which is currently Stage 1 https://github.com/tc39/proposal-signals
- And this proposal: https://github.com/WICG/webcomponents/issues/1069 which is basically lit-html in the browser
It's not a no-build option though.
Especially helpful as applications become larger and a debugger becomes necessary to efficiently track down and fix problems.
I've been a front end developer for 25 years. This is also my opinion.
However I don't get to dictate fashion in developer stacks.
Code written for a web browser 30 years ago will still run in a web browser today. But what guarantee does a build step have that the toolchain will still even exist 30 years from now?
And because modern HTML/CSS is powerful and improving at a rapid clip. I don't want to be stuck on non-standard frameworks when the rest of the world moves on to better and better standards.
Will it? - My browser doesn't have document.layers (Netscape) It seems to still have document.all (MSIE), but not sure it's 100% compatible to all the shenanigans from the pre-DOM times as it's now mapped to DOM elements.
https://www.spacejam.com/1996/
Those (document.layers and document.all) were both vendor-specific, neither were part of the w3c. I don't recommend ever writing vendor-specific code.
The w3c and standards have generally won so it's easier than ever to write to the standard.
Then Google Closure Compiler came along which added type safety via JSDOC and TS came along with (TS)JSDoc support and it's own TS syntax.
The community chose native TS and Google Closure compiler slipped away into the background.
So (TS)JSDoc support is a relic from when Microsoft was trying to get market share from Google.
Today in 2025, TS offers so much more than the (TS)JSDoc implementation. Generics, Enums, Utility types, Type Testing in Vitest, typeguards, plus other stuff.
Today I use TS. I also use plain JSDoc for documentation. e.g. @link and @see for docs. Or @deprecated when I'm flagging a method to be removed. @example for a quick look up of how to use a component.
TS and plain JSDoc are both important together. But (TS)JSDoc alone, is a relic of the past.
This was my main impetus for writing this article. Modern JSDoc uses the TypeScript language service. You can use generics, utility types, typeguards (including the `is` keyword), regex parsing, etc all with just JSDoc.
I used these features extensively (especially generics) in a personal project and managed to do it all in JSDoc.
Types for classes are poor and often you'll find yourself creating a `.d.ts` file or `.ts` file to export non trivial types - however the target file doesn't know how to consume them.
Regardless, I hardly consider that a "missing basic capability"
I don't know what you mean about types for classes being "poor". Types for classes work exactly the same way
The alternative is to do an inline `const foo = /** @type {import('./foo').x} */ ({})` however this gets messy, repetitive and it's difficult to use algebraic types (e.g. `Event & { detail: string }`)
https://github.com/jsdoc/jsdoc/issues/1917
https://github.com/jsdoc/jsdoc/issues/1917#issuecomment-1250...
```js
/**
* @type {{
* slug: `${string}_${number}`;
* id: number;
* } & { status?: [code: number, text: string]; }}
*/
const example = { slug: 'abc_34', id: 34 };
is the exact equivalent of```ts
const example: {
slug: `${string}_${number}`;
id: number;
} & { status?: [code: number, text: string] } = { slug: 'abc_34', id: 34 };
For TS-specific keywords like `satisfies`, there's a corresponding JSDoc keyword like @satisfies. Generics use @template.Is there any specific feature you think is not supported? I'm sure I could work up a TS Playground example.
Yeah, uhm, most of what you've been posting? :). That JSDoc example above gives:
ERROR: Unable to parse a tag's type expression for source file /Work/lol-jsdoc-why/index.js in line 1 with tag title "
type" and text "{{ slug: `${string}_${number}`; id: number;} & { status?: [code: number, text: string]; }}": Invalid type expre
ssion "{ slug: `${string}_${number}`; id: number;} & { status?: [code: number, text: string]; }": Expected "!", "$", "'", "(",
"*", ".", "...", "0", "?", "@", "Function", "\"", "\\", "_", "break", "case", "catch", "class", "const", "continue", "debugger",
"default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "implements", "impor
t", "in", "instanceof", "interface", "let", "new", "null", "package", "private", "protected", "public", "return", "static", "supe
r", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "with", "yield", "{", Unicode letter
number, Unicode lowercase letter, Unicode modifier letter, Unicode other letter, Unicode titlecase letter, Unicode uppercase let
ter, or [1-9] but "`" found.
Edit: Also, your first edit says Webpack switched from TypeScript to JavaScript, but Webpack source was never written in TypeScript.https://www.typescriptlang.org/play/?#code/PQKhCgAIUgBAXAngB...
You are attempting to generate documentation from jsdoc comments using an npm package that is also called "jsdoc". Ofc in this case "JSDoc is not TypeScript". That package only supports the subset of JSDoc that is relevant to it. Though I believe you can use TypeDoc instead if you want to generate documentation from JSDoc that contains typescript types.
In the post I made it explicit that I'm talking about intellisense, developer tooling, type checking etc. You can run `tsc` to do typechecking on a project typed with JSDoc like the examples I've given throughout this thread just fine.
I guess the difference here is I'm coming at this from the perspective of "what is TypeScript used for. Can JSDoc comments substitute that". And the answer is almost completely yes.
Also tbh I've never met anyone that uses that package to generate API docs. I don't think it's a very modern package: https://github.com/jsdoc/jsdoc/issues/2129
Your post is actually one of the more accurate ones compared to others that say "you don't need typescript" with the big caveat that you actually need a whole lot of the typescript ecosystem to make JSDoc work.
I just wish there was an official handover, or a more clear delineation between JSDoc and Typescript JSDoc Extensions.
But given that JSDoc doesn't have any sort of formal spec, I think the distinction you're making is more of a historical than a technical one.
JSDoc has been around for more than twenty years and most implementations have never had most of the capabilities you’re describing.
It is actively misleading for you to say that JSDoc has these capabilities when you’re referring specifically and exclusively to TypeScript’s implementation of JSDoc, or you could say TypeScript’s alternative JSDoc syntax. Closure always used language like that in their documentation, and explicitly called out that they had diverged from standard JSDoc, as they should have. TypeScript’s own documentation sometimes refers to it as their superset of JSDoc, again recognizing that “JSDoc” actually does mean something specific and different.
The fact that there may not be a formal technical spec doesn’t mean you’re not wrong and it’s preposterous to suggest that.
There was established tooling and documentation going back 25 years, and it doesn’t somehow not count just because they didn’t give you a formal grammar…
Wishful thinking on my part that an alternative solution for JSDoc based type checking exists :)
> Today in 2025, TS offers so much more than the (TS)JSDoc implementation. Generics, Enums, Utility types, Type Testing in Vitest, typeguards, plus other stuff.
None of that is true! Please don't share misinformation without looking it up first.
2. you can have navigation that goes to typescript file instead of definition, just arrange your exports in package.json correctly (first ones take precedence)
Since any TypeScript type can be expressed in JSDoc, I imagine you're mostly thinking of generics. At least that was my main sticking point. JSDoc does actually have generic slots with the @template tag. Actually using them in practice is a little unintuitive but involves typing the return type. E.g. for a function it'd look like this:
/** @type {ReturnType<typeof useState<Book[]>>} */
const [books, setBooks] = useState(); /**
* @typedef {object} Dog @extends Animal
* @property {string} childProp
*/
But I don't really use that feature in TypeScript. Instead I rely on `&`. This works in exactly the same way in JSDoc.Also if you're curious about the equivalent of `extends` in generic slots, here's an example I have from a different project
/**
* @template {Record<string, unknown>} [T=Record<string, unknown>]
* @typedef {{
* children?: NewickNode<T>[];
* name?: string;
* length?: number;
* data?: T;
* }} NewickNode
*/
The generic slot here, T, is "extended" by Record<string, unknown>. The equivalent in TypeScript would look like type NewickNode<T extends Record<string, unknown> = Record<string, unknown>> = {
children?: NewickNode<T>[];
name?: string;
length?: number;
data?: T;
};Everyone's complaining about "the build step" but the build step is just an eye blink of stripping out some things that match a regex.
This is inaccurate on multiple counts. First of all, you can still run tsc with JSDoc if you want a hard error and you can still use strict mode with JSDoc. Your tsconfig file governs JSDoc-typed code just the same as it governs .ts-typed code. In both cases you can also ignore the red squigglies (the exact same red squigglies) and end up with runtime errors.
Nobody is advocating for reduced type safety or increased runtime errors.
I also think there are many valid reasons to loathe a build step (like not dealing with the headache that is the discrepency between the way the TS compiler deals with import paths vs js runtimes).
All that being said, I'm not really trying to convince anyone to stop using TypeScript. I'm simply pointing out that using JSDoc is using TypeScript. It's the same language service.
All non erasable constructs won't work as well of course but playing devil's advocate you could explicitly state that you're interested in erasable constructs only because ie. 1) that's what typescript should be doing from day 1 and/or 2) it seem to be the future with ie. nodejs adopting built in type erasure support.
JSDoc also allows you to type stuff in-line. For example I often have to type an empty array like so:
const [books, setBooks] = useState(/** @type {Book[]} */([]));
If you have a tangible example of a problem you've run into, I'd love to walk through it.https://www.typescriptlang.org/play/?filetype=js#code/PTAEAE...
Almost equivalent typescript code:
https://www.typescriptlang.org/play/?#code/C4TwDgpgBA6glsAFg...
(I had to make it a little bit different from the JS code to make it compile)
(Well, this is not exactly about arrow function I guess. I remembered that part wrong.)
Note that I cannot make the type check in JS code to pass. Whatever I do, there is always a error. Meanwhile, it does not take much to TS code to work.
https://www.typescriptlang.org/play/?filetype=js#code/PTAEAE...
Hover over the variables and you should see that the type inference is working just the same as in your TypeScript example
I do think it illustrates a problem with TypeScript's support for JSDoc though. You see, I started with the code in JS and could not make it work, after which I translated it to TS. In JS/JSdoc, "@callback" is the "idiomatic" way of defining a function callback type with JSDoc. (It also makes it easier to add documentation for each parameter if necessary.) And indeed, @callback works the most of the time, except in such cases where these JSDoc tags don't work nicely together, and these alternatives become necessary.
My brain definitely works in TypeScript so I tend to translate from there. I definitely consider myself more familiar with TypeScript than with JSDoc, but sometimes (e.g. here) that's a benefit not a weakness
also not a full solution - for .d.ts types to be available globally without explicit import the .d.ts file itself cannot use any imports/exports. this means you can't reuse types from other places to construct your types. you can workaround this by explicitly importing .d.ts in jsconfig/tsconfig but you're still left with other issues.
those types do actually become globally visible everywhere polluting global namespace which is bad in itself
there are no guarantees about them being in sync with actual code, which violates the whole point of using type safety.
they don't solve cases where you need typescript inlined functionality locally in your code or to perform assertion with satisfies operator etc.
TypeScript utility types are available in JSDoc. You can pretty much copy-paste any typescript Type/Interface into JSDoc
This isn't really true anymore, they have systematically added pretty much every type system feature to the JSDoc-like syntax.
You keep repeating this throughout the thread. Can you give an example?
export type SemVer = `${number}.${number}.${number}`;
Could you extend it to work with regex groups like:
export const SemVerRegex = /^(?<major>0|[1-9]\d)\.(?<minor>0|[1-9]\d)\.(?<patch>0|[1-9]\d)(?:-((?:0|[1-9]\d|\d[a-zA-Z-][0-9a-zA-Z-])(?:\.(?:0|[1-9]\d|\d[a-zA-Z-][0-9a-zA-Z-]))))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
Could the groups be extracted so you the type back if you ran the regex on a string like: "1.2.3", or "1.2.3-prerelease"?
/** @typedef {`${number}.${number}.${number}`} SemVer */
Here's a playground: https://www.typescriptlang.org/play/?filetype=js#code/PQKhAI...The second part of my comment is a value yes, but it's implicitly typed by typescript automatically. I was asking about how to use that type (and it's internals) in jsdoc.
ok i didn't think about this, that's an underrated benefit
Prefer Go To Source Definition
Makes `Go to Definition` avoid type declaration files when possible by triggering `Go to Source Definition` instead.
It was added like 3 years ago which was probably a bit too late, not even sure why it's not the default. (File size?)
IDEA adds its own analysis on top of that provided by the language server.
Works on JS + every variant of type definitions I've ever seen, among many other things (not only programming languages, but also database objects, etc).
IDEA implements its own analysis and doesn't use tsserver at all. Its semantics diverge in subtle ways, I believe in both directions (some code that tsserver considers valid IDEA will consider invalid and vice versa).
"is" is doing a lot of heavy lifting there: JSDoc and TypeScript are two different ways to explicit prescribe typing in a way that tooling can use to determine correctness. The TS syntax is _far_ more powerful, but JSDoc can do most of the common TS use cases, for folks who want to stay in JS land while still benefiting from type tooling (either invoked or straight up built into the IDE).
As I pointed out in the article, the "tooling" is exactly TypeScript language services. If you are using JSDoc and you get squigglies or intellisense or any other similar features, you are using TypeScript.
You can copy-paste basically any bit of TypeScript into a JSDoc comment and it will work. JSDoc supports any non-runtime feature of TypeScript (so not enums). Even generics! You can even reference TypeScript utility types!
The whole point of this article was to correct the idea that JSDoc is not TypeScript. It absolutely is! There's almost nothing you can't define in JSDoc that you can't define in a .ts file. Albeit with a sometimes clunkier syntax
JSDoc is a comment formatting standard.
The tooling used to parse it is independent.
In the same vein, a file of ISO C++ (take any version) code is not "GNU C++".
This is true in the same way you are "using" C++ if you are on Windows. When most people say "use XYZ language" they mean "are personally writing code in XYZ language" rather than "under the hood my code is transpiled to this other language I don't write in"
It might not have been so originally. It might still be possible to do differently. But in practice today you are getting Typescript in your JSDoc with the out of the box tooling that is everywhere.
If you define a type in a file with @typedef, it is automatically exported and there is nothing you can do to control that: https://github.com/microsoft/TypeScript/issues/46011
I tried making a library this way and lacking control over the visibility of the exported types was really painful; it made my intellisense awful because every type I defined at the root was exported from the library
Granted they initially weren't down that path, but they course corrected it on time, and not much people use stuff like enums in new code.