A recent trend has shaken up the JavaScript-TypeScript community: the anti-build-step movement. For over a decade now, a build step1 has been widely considered to be a necessary best practice in modern web development. Now more than ever, this seemingly dogmatic reality of the web development experience has been challenged.
There are many reasons for this sudden change of heart.
- The Fresh framework by Deno cited an improved developer experience due to tighter feedback loops.
- The Svelte team followed suit but motivated by the maintainer's developer experience as they migrated the project away from TypeScript in favor of plain JSDoc comments for type annotations instead.
- Most controversially, the Turbo framework dropped TypeScript support altogether after assessing that strong typing was the culprit behind poor developer experience.
One common thread ties everything together: developer experience. In this article, however, I will not argue whether TypeScript is still relevant today.2 Instead, I choose to reflect on my own journey of stepping away from TypeScript—but for a completely different reason: consumer semantics!
TypeScript Need Not Apply
TypeScript prides itself as a superset of JavaScript. As a matter of fact, TypeScript owes much of its success to this key design decision. The classic migration guide for renaming .js
to .ts
considerably lowered the barrier to entry as teams gradually migrated their codebases. But, it is exactly this design decision that also made me realize where TypeScript isn't necessary!
With TypeScript being a superset of JavaScript, there exists a subset of the TypeScript language that's just plain old JavaScript. As it turns out in my experience, this also happens to be the subset of TypeScript that I deal with most often—especially in application code!
Consider the following example that features two TypeScript files: add.ts
(simple adder library) and main.ts
(application entry point).
// add.ts
export function add(x: number, y: number): number {
return x + y;
}
// main.ts
import { add } from './add.ts';
console.log(add(1, 2)); // 3
In add.ts
, the presence of type annotations makes TypeScript a necessity. On the other hand, a cursory inspection of main.ts
shows us that its syntax is purely JavaScript! At its current state, main.ts
need not be TypeScript; main.js
is sufficient.
This central motivating example led me to re-evaluate my relationship with TypeScript. It gradually became apparent to me that TypeScript is best suited for library code while JavaScript is more appropriate for application code.
EDIT: I would like to clarify here that when I say "library code", I do not necessarily mean published NPM packages. "Library code" can refer to internally linked sub-projects in a monorepo/workspace. One may even consider imported files from the same project to be "library code". The point being: "application code" is the consumer of "library code" wherever that may be imported from (e.g., NPM, workspace, files, etc.).
Specifically, consumer code that can solely rely on type inference for type safety (i.e., no type annotations required) may thrive on JavaScript alone. This is not the case for library code which export functions that require type annotations—especially for parameters. One may even take this guideline to the extreme by removing return types from function signatures altogether in favor of return-value type inference. This is generally considered to be poor practice, though.
Nowadays, I isolate library code more aggressively. One may enforce a scheme (for example) where UI components are consumers written in JavaScript while the more complex business logic, data fetching, and data validation are imported from a TypeScript library. Of course, the libraries may be consumers themselves, in which case they may be written in plain old JavaScript as well.
Observe that such a scheme is desirable not only because it is a best practice that encourages more testable architectures, but also because it makes the separation between the JavaScript world and the TypeScript world more intentional. That is to say, I choose to write JavaScript (in consumer code) because type inference is sufficient. But, I also choose to write TypeScript everywhere else when type annotations, type aliases, and interfaces are necessary.
JavaScript First, Then TypeScript
The .js
extension is no longer an indicator of antiquity, but a declaration of sufficiency in language semantics. I prefer to write plain old JavaScript because the .js
extension presents itself as consumer code. The .js
extension is a deliberate communication of the consumer semantics.
For more advanced cases, I upgrade from .js
to .ts
, but keep the TypeScript surface as minimal as possible while isolating it from the rest of the application code. Arguably, this is exactly what .d.ts
declaration files are for, but I admittedly find that the developer experience for inline implementation files is more ergonomic (and less error-prone!) than duplicating function signatures in .js
and .d.ts
files.
Overall, I still strongly believe in TypeScript's value to the JavaScript ecosystem.3 Nowadays, I just choose to write JavaScript first wherever possible, then upgrade to TypeScript as a last resort.
-
This includes all sorts of transpiling, compiling, tree-shaking, code-splitting, bundling, etc. ↩
-
In fact, I am a huge fan of the type-safety conveniences and guarantees that TypeScript enables. ↩
-
Even without the TypeScript syntax, the benefits of type safety also extends to JSDoc annotations (which are powered by TypeScript analysis anyway). ↩
Top comments (68)
Your assessment, while very interesting and appealing is ultimately incorrect because you are taking the route of the "weaker part", making your entire conclusion invalid. "Weaker part" is following the weaker of your observations. "Weaker" means "some", when there is an "all" part you can follow instead. Quickly putting an example:
All women are human.
Some humans are doctors.
Conclusion: All women are doctors.
Because I followed the weaker premise, my conclusion is incorrect. This is what happened in your case: Only some consumer code depend on libraries. Not all of it.
Let's also remember that TypeScript has another great feature: Providing new syntax not yet implemented in browsers and JS runtimes. You can take advantage of upcoming JS features when writing TypeScript because TypeScript then transpiles those features according to the target language of your choice. Therefore, your other error is minimizing TypeScript to a mere typing mechanism.
I fail to see what you mean by this. Would you mind further clarifying how my line of argumentation fits the form that you observed?
But to clarify my position, I argued that "JavaScript is more appropriate for application code" where type inference is sufficient. Otherwise, in cases where type annotations, type aliases, and interfaces are necessary, I delegate and isolate the TypeScript-specific code to libraries (that are imported into the solely-inference-reliant JavaScript code). I thus concluded that "TypeScript is best suited for library code while JavaScript is more appropriate for application code".
Ah, I admit that I was not clear with the context I am working with here. For clarity, I assumed a project setup that uses a bundler (e.g., Vite) such that
.js
files get transpiled to targeted editions anyway. I thus "minimize TypeScript to a mere typing mechanism" under the assumption that the work of downgrading syntax and polyfilling is delegated to another plugin in the bundling pipeline (e.g., Babel).But if we assume that TypeScript the compiler is indeed the primary means of transpilation, then I must concede to your point. TypeScript the compiler would be necessary for that particular situation. Everywhere else, I believe I am fully justified in "minimizing TypeScript [the language] to a mere typing mechanism". Please do let me know if I miss out on other use cases, though.
I am a back-end developer, so the world of Babel and company is not my forte. Based on your claim, I asked Bing's AI to clarify this for me. I asked "Bing, can Babel, Vite, esbuild or any other tool do what TypeScript does? Can they polyfill and re-write code of not-yet-released JavaScript language features?". The response:
It seems that not even Babel can replace TypeScript fully when it comes to not-yet-released JS syntax/features. Is the AI correct?
Your train of thought derailed because you are incorrectly assuming that all code that is TS-dependent can or will be encapsulated in a library. This, while theoretically possible, is never done in practice. You could attempt to advocate for this practice, but I don't see anyone calling code that doesn't follow it a bad practice or anti-pattern.
For example, a typical business app that connects to an API needs to type the API responses. Will people create an NPM package to declare the application models? Unlikely. So we will write TS in-app. Furthermore, there will be some business-related code that we will be writing directly in the UI app, most importantly for security. This will be very specific to the app and most likely it will declare functions and services that need typing. Will devs be encapsulating this in an NPM package? No. It's only useful to this one app. It's a waste of time.
Now, to be fair: Since I suppose it is strictly possible to follow the encapsulation pattern you propose, I'll just say this is logical, but not practical.
Perhaps this is true for the extra bleeding edge features such as the
using
keyword. Nevertheless, the official TypeScript Handbook notes thattsc
is typically meant for type-checking. That is to say, transpilation is an incidental feature rather than a first-class use case. More advanced transpilation and polyfilling still falls on the hands of build tools.To that end, I'd say the AI is correct with emphasis on the caveat that only the extra bleeding edge features are unavailable (which is a fair limitation to be honest).
Ah, I see where we have a misunderstanding now. When I say "library code", I mean this in the monorepo sense of the word. Taking Rust's package manager Cargo for example, we have first-class monorepo support, which enables us to treat a project as multiple sub-projects. The entry point project is the "application code" which imports the "library code" from the other sub-projects.
Now, this distinction is important because I believe we misunderstood each other when I say "library code". In this article, "library code" can be some locally linked sub-project in a monorepo/workspace or some other file that we import from the same project. The point being: "library code" is not necessarily a published NPM package.
I hope this clears up the confusion.
I guess it clears the confusion up, but I still think it is not practical.
I would make a
services
folder to accommodate data services, and I need to type those. Just because they are typed I won't create a newpackage.json
to make it a sub-project. It is very impractical.What I mean to say is: The application will have application-specific code that is only useful in said application. Monorepos are overkill, in my opinion, for the gains. Speaking of which: What are the gains? Using
.js
files? Is that it? Transpilation time? What are the gains, exactly?If you are promiting "developer experience" here, what is that experience, exactly? To write code in a
.js
file instead of a.ts
file? Code that would look identical in either? I think I am not understanding the gains, or maybe indeed the gains are minuscule. Do tell me.I totally agree. That is why I give some leeway in my definition of "library code" in that it can literally be any code you import from the "application code".
Of course! And for that reason, I reiterate that I give leeway to my definition of "library code" so that it also includes files of the same project. As long as they are imported somehow into the "application code", we may as well consider them to be "library code".
The main thesis of the article is that I have begun to treat the
.js
extension as a deliberate communication of the developer's decision to limit the application code to solely rely on type inference for type safety. In cases where type inference is insufficient, I upgrade the.js
file into a.ts
file but as a last resort considering the most minimal surface possible.The "developer experience" that this scheme provides (at least in my experience) is that I can tell from the file extension alone which files in the project are "[dumb] application code" (for lack of a better word) and which files are the more interesting and meaty "library code".
Consider the application entry point for example. Seeing a
main.js
file certainly jumps out more than amain.ts
file in a sea of.ts
library code, right? Here, I find that the.js
extension now takes on a new meaning: "I am consumer code!"Clearly, you have a well-organized mind that thrives in the details. I think you are like me 7 or so years ago. I think you are over-selling this because there isn't much to sell. I say this with admiration, however. Your train of thought demonstrates cleverness. You just need to apply an extra step: Practicality.
I understand what you say fully, I think. I just see no practical, useful gains in it. To see whether code is "consumer code" or "witty, complex code" by means of a file extension is nowhere in my to-do's.
Thank you! That means a lot to me.
This is totally fair. I must admit that the file extension semantics are quite subtle. It is not immediately apparent why such a mix of
.js
and.ts
files exist.Nevertheless, I personally find them very helpful when navigating codebases nowadays. Onboarding new developers who use VS Code (for instance) renders file icons according to their file extension. The distinct yellow JS icon versus the blue TS icon is a neat visual signal for possible entry points and consumers (e.g., perhaps UI components?) in the project.
The industry is moving away from that; esbuild → Content Types → TypeScript:
“ …esbuild has built-in support for parsing TypeScript syntax and discarding the type annotations.”.
Type discarding bundlers are used to accelerate the feedback loop that otherwise would be significantly slower if they were limited to using
tsc
—especially in large code bases.Relying on TypeScript specific features (e.g. decorators) essentially blocks that build time optimization.
Using non-JavaScript TypeScript features comes with significant tradeoffs. By and large TypeScript is being used as a JavaScript type linter, either with it's own “more convenient” syntax or in combination with JSDoc.
Using TypeScript as a “language” presents it's own risk.
The point being? I could not infer.
I completely agree. However,
tsc !== TypeScript
. The compiler of choice is not the language itself. The language still carries many benefits, regardless of the compiler/transpiler of choice. So I guess I continue to miss your point.Regarding build times: Yes, using TypeScript adds a "transpilation/compilation" step to the CI/CD. But how much are we talking about? Does anybody have benchmarks lying around?
… TypeScript is being used as a JavaScript type linter [not as a compile-to-JS language].
I'm not talking about CI/CD. I'm talking about during normal development and/or micro testing; development approximating REPL-driven programming (… a set of tools and practices for programming that emphasize fast and rich feedback). It allows you to bypass many specious TS-errors until you are ready to address them in earnest and run a «full blown» type check, not just stripping the type annotations.
“I often wish I worked in a language where I could start dynamic and end statically”
Ok, I saw the video. OMG. Is people like this now? In the video:
Is C# a linter because it produces MSIL? OMG. Clearly this person and anyone that backs him up has lost the depth in their train of thought. Pretty much the entire video should be deleted.
People reading this: Did you know that before C, languages were untyped? Do you know how types are really used by compilers? My guess is No, you don't know because you keep minimizing the importance of types. I'll leave this as homework assignment to you all, avid followers of the "kill TypeScript because typing is linting" movement.
This already exists and TypeScript does not interfere with it. Literally every
Vite + TS + <Technology>
project uses esbuild to provide this experience. Is TypeScript getting in the way of this? How? When? Where? I don't see the relation between TypeScript and the inability to have REPL-like development experiences. It sounds to me that people are thinkingtsc
to be mandatory when using TypeScript. Is not.The (lack of) type checking speed of
tsc
. Nobody uses anything else for type checking as all the alternatives strictly strip but do not type check (and perhaps even transform).As I alluded to before, TypeScript was never about type safety, it's about “somewhat safer types as long as it doesn't cause the developer any additional effort”— i.e. developer convenience first.
It has always been my contention that people who care about types would use something more like ReScript rather than TypeScript and simply accept the burden of managing JS interoperability. TypeScript makes many (permanent) compromises in order to allow progressive adoption on a JS code base and to consume pure-JS libraries.
It's too far gone for that. Again: TypeScript's popularity has nothing to do with types but everything to do with developer convenience via Intellisense. Accepting anything else is just being in denial.
Please read the documentation:
“Note that Vite only performs transpilation on
.ts
files and does NOT perform type checking. It assumes type checking is taken care of by your IDE and build process.”Vite achieves it's level of experience by bypassing the type checking stage entirely. Most of the time people just fix the issues as they see them in their editor via LSP. Then in CI/CD
tsc -noEmit
is used to catch any errors that may have been missed.PS: Deno skips type checking by default as well.
Ok, but this is
tsc
, the compiler, notTypeScript
, the language.Intellisense is the live reflection of the compiler. Wanting this live reflection is nothing to be ashamed of. To say that you "want Intellisense but not compilation" is impossible, as Intellisense is completely predicated in the compiliation process.
Yes, and there's nothing wrong with that. You, as a developer, should be fully aware of this. You, as a developer, once you are happy with your product, should then kick off the TS compiler to ensure your project is error-free. You, as a developer, should include TS compilation in CI/CD as well. How does any of this preventing REPL-like experiences?? The concept of TS somehow getting in the way simply escapes me. Because most people don't do it properly, then it is TypeScript's fault and must be stopped? Or are people saying that this is "a bad thing" because somehow compilation should be in every single scenario every second of the way or not exist at all? That would also be non-sense to me.
So?
Some would argue that
tsc
isn't a compiler in the original sense.In the early days of C++, Cfront was identified as a cross-compiler largely because it did not change the granularity/level of it's output―it performed a code transformation but stayed on the same high level of language. Later the term transpiler was coined for transforming-compiler underlining that the transformed output wasn't operating at a finer granularity (e.g. assembler).
Given that it is possible to just strip the TypeScript annotations with minimal transformation, TypeScript is just a
tsc
syntax for JavaScript rather than a language. Admittedly TS JSDoc would have been the more authentic solution but that would have never gained any widespread adoption because it would have been too inconvenient for application developers.TypeScript has always been squarely targeted at application developers rather than library authors which always had other ways of providing the “types” to support their users development environment's code completion:
“TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the programmers at Microsoft could bring traditional object-oriented programs to the web. As it has developed, TypeScript’s type system has evolved to model code written by native JavaScripters. The resulting system is powerful, interesting and messy.”
This is the step that is considered "type linting" (aka static type analysis/checking).
It emphasizes how TypeScript is just lipstick on JavaScript.
This is so harsh! Made me laugh. 😄
Ok, so this feud is making my head spin. On one side, there is people saying TypeScript should not exist. On the other, there's people like me saying TypeScript provides value. The main issue is that people whine about their source code being more difficult to maintain for something that gives them nothing. Am I correct so far? If yes, these people say that, because of its alleged zero value it should not exist. This is the overall view.
Now, especifically:
I think we can agree that #2 is false. Let's see about #1.
One of the things that I like the most of TypeScript because I can state the data types, is catching missing logic. A very simplified example:
Because I can specify the data types, both Intellisense and the transpilation step (
tsc
) will tell me that I forgot the fact thatvalue
could beundefined
.This makes me correct my code to:
Do linters do this? Is this kind of feature available in eslint? Or is it something that only TypeScript can do?
I don't see how. Starting with TypeScript the REPL-like experience can only be realized by using a type stripper. There is no
--transpileOnly
flag intsc
so the type checking (executing in a JS runtime) bogs the feedback cycle down. The only other way to bypasstsc
is to write plain JavaScript an annotate it with TS types inside JSDoc annotations.Aside:
Nobody is going to argue that React isn't “popular” (whatever else one might think of it). React isn't developed with TypeScript. The DefinitelyTyped types are maintained by the React team but React itself is typed with Flow. AFAIK Flow was developed with OCaml which is fairly popular with compiler writers because it's binary execution speeds are "close to a compiler written in plain C". It stands to reason that Flow never had the
tsc
speed issues so it never slowed the React team's development flow.Many people prefer Flow over TypeScript however Flow never caught on, perhaps because MS Windows wasn't supported until late 2016 by which time DefinitelyTyped was already firmly established and now the Angular 2 release further increased general interest in TypeScript.
In some ways one could say that TypeScript is more a product of a "productivity culture" while Flow seems to lean more towards the outcome of an "engineering culture" (though I would classify React itself as a product designed for a "productivity culture").
People value TypeScript for the developer convenience it provides in their development environment, primarily VSCode (or any other LSP supporting environment).
People valuing reliable static type analysis have to look elsewhere.
Library authors requiring strict control over the runtime JavaScript code (for whatever reason) elect to use TS JSDoc primarily to deliver consistent TypeScript types for their users, with the side benefit of (only) on demand
tsc
static type analysis.Linters in the original sense no.
But in the web dev world the concept of static analysis tools for dynamic languages was virtually unheard of (example Erlang: dialyzer; Elixir: dialyxir).
Flow had the right idea: be a static analysis tool that supports an inline type syntax and be fast enough to never get in the way.
TypeScript started with ambitions to be a language but the enum fiasco just drives the home the point that deviating from the JavaScript baseline only comes back to haunt (and hurt) TypeScript.
So in the web dev world static analysis tools were explained as "type linters".
Ok, I see that you are incapable of admitting that
tsc !== TypeScript
, which makes you repeat over and over the idea that esbuild is a "trash remover". I guess we will just have to agree to disagree. REPL-like experiences are possible today for TypeScript-powered projects, whether you want to admit it or not. TypeScript is in no way an impediment to this.Furthermore, I have demonstrated fully that TypeScript does bring value to the table using a 3-line example. I have fully demonstrated what probably is the greatest benefit of this language. Whether or not you want to admit it, is again, up to you.
I believe I'll just let other people reading this judge for themselves. I will end my participation here saying: Make your own minds. Always be critical to what you hear and read. I personally think this whole movement has no reason to be, and that TypeScript is an invaluable asset that helps developers produce better code faster.
Value that can be delivered with static type analysis alone. No language required.
The point is that Flow delivers exactly the same value, according to some people at a higher level of quality, without proclaiming itself as a language but simply as a “static type checker”.
So:
Betting on TypeScript tooling as a static type checker (TS-as-a-devtool) is where the current value is.
Using it as a language is risky especially for features that are not on the current list of TC39 proposals.
Ok, so clearly you want to continue arguing. The problem I have, and I say this without trying to be antagonist, is that you are like Alexa:
If you can actually stay on topic, I'll be glad to continue the discussion. If you, however, are incapable of reasoning within the boundaries of the topic, I'll just make myself scarce. I am not, and will not be, diverted by off-topic subjects. I am also not interested in other people's words. I'm interested in your own and my own. We are the ones in this discussion, not others. All your quoting is irrelevant to me because I make my own mind and not comply with others just because of their alleged reputations.
I'll start: I love, as many of us do, VS Code. Flow and VS Code are not a thing. Therefore, it is not going to be a thing for us to move to Flow. Let's just drop Flow. Let's talk about things that could actually happen.
Let's discuss the very important matter: TypeScript exists, is a language and comes with
tsc
from Microsoft.tsc
is not very performant. Is this a "language" feature? It is not. Why? Anyone could make a better transpiler and the language itself would be unaffected. Agreed? If yes, you must also agree by extension, thattsc !== TypeScript
. Yes? No? Discuss, but stay on topic. I am not interested in stories about the history of pictographic communications in caves found in the Middle East that date from 15.000 BC.Faster transpilers have already been delivered.
As far as I'm aware
tsc
(and LSP) is the only tool capable of type checking the TypeScript syntax and nobody is working on changing that.And as to your example:
TS playground
tsc
• variable declaration space
• type declaration space
Fixed:
Which just emphasizes again that TypeScript adoption/popularity is primarily about developer convenience rather than safe types.
Great! We are getting somewhere now. Tell me: Is the benefit coming from JsDoc, or Typescript enforcing JsDoc? Because in jsfiddle.net:
I get no feedback.
tsc
or LSP parse the type syntax inside the JSDoc annotation to fill in the "type space gaps" in the JavaScript code.tsc
or LSP determine whether the code type checks.Ok, yes. I see your point more clearly now. Because JsDoc goes into comments, there's no stripping to do. That is most certainly a clear advantage, probably the greatest advantage over TypeScript, no doubt.
So, we have two options for type safety: One that uses comments, and one that doesn't, and people say the latter can go away. I guess it is only logical. Unless, that is, that TypeScript (the latter) has something else in its favor, and there is.
TypeScript can bring new JavaScript features to life faster. Yes, you have already argued about how that could be problematic. Still, it is a feature that only TypeScript provides. I believe in the democratic process of every developer to choose their tooling, and I truly believe that this is one of those cases. It is no small thing, in my opinion. Yes, there will be people that say this doesn't justify the grievances of transpiling. I get that.
At this point, I must admit you have me half-convinced. See? All that ranting about Flow and all those quoting got you nowhere with me, but your succinct demonstration got me thinking!
I could probably ask you about the capabilities of JsDoc, but I suppose that I should just investigate that on my own to determine whether or not I should opt for JsDoc. For example, it is very difficult for me (or impossible, really) to imagine I can define the types I need for an entire project with just comments. I am a back-end developer, so my experience with JavaScript is not very extensive. My main concern right now is: Can I define the data models of an entire application in JsDoc, centrally in one location and use the definitions in the rest of the project?
UPDATE: Question: Is JsDoc a thing by itself? Or must it always piggyback ride on TypeScript? I must confess I'm confused about this topic for the first time. I am concluding that in VS Code all that is needed is the language server. If this is true, and JsDoc can provide an alternative to at least most of TypeScript features, it is something I must learn ASAP. The one thing that would seriously worry me is not having a CI/CD method of making sure developers did their work of honoring all of the Intellisense feedback.
This thread is quite interesting.
Here is my personal opinion, TypeScript is definitely prone to adding aggressive syntax sugar due to its compilation-based nature.
And these are a kind of auto code.
However, the basic JS syntax has been sufficient since ES6.
Some features may require polyfills, but if you only use the basic JavaScript syntax, TypeScript will simply perform type checking, which means that you don't even need to copy my source code from the src folder to the dist folder.
Of course, TypeScript provides a watch feature, so in many cases these limitations will not be felt significantly. But it's not zero.
Perhaps the biggest advantage of JSDoc is that you can see the actual implementation code using VSCE's ctrl + left click while using the library.
This is not possible with js or compiled ts.
Perhaps the JSDoc example in my Stackblitz will answer your questions.
Yes but the intent is only to document, not type check (e.g. like Doxygen).
That is why I go out of my way to use the term TS JSDoc when referring to applying TypeScript types to JavaScript code via JSDoc. I suppose the choice of using JSDoc was influenced by the fact that Google's Closure Compiler already used JSDoc to carry type hints.
A large segment of the TS user community is. Back in May it became very clear that many did not understand the tool chain they work with day-to-day when there was a social media uproar against SvelteKit moving to JSDoc when in fact the team:
It is the defacto choice but lots of editors/IDEs are capable of supporting the language server protocol (IntelliJ WebStorm, neo/vim, etc.).
Be advised, in my experience using TS JSDoc effectively requires a higher level of tooling knowledge/competence than just using TS syntax in an LSP supported editor. So typically it makes sense to spend ones time first with the pure TS syntax because it (at least on the surface) seems more familiar coming from the likes of C#/Java.
But TypeScript's type manipulation syntax (and the way it extracts information from value space into type space) can take some time getting used to.
That isn't to say the TS JSDoc isn't without its annoyances.
Many simply prefer their types conflated with the implementation because that is what they are familiar with from other languages (Haskell/Elm converted me to separating types from implementation) and they resent having to type more (Typing is not a programming bottleneck).
Having to juggle between
undefined
andvoid
in the type declarations until it finally works is tedious (tsc
seems to randomly shallowundefined
from sum types otherwise). And in case you didn't notice I couldn't getvalue ?? 1
to type check in your example leading me to usevalue ? value : 1
instead; so you are sometimes forced to write your code a certain way for the type checking to work as intended.That is what
tsc -noEmit
is used for. For whole project type checking there is no way to get aroundtsc
right now but if it is confined to CI/CD it isn't a big issue.In practice you don't.
You will still have
*.ts
files (see this comment) to hold shared or complex types but only for declaring types or extracting type information from JS files (another complaint against TS JSDoc is the frequent separation of TS types and JS implementation 🤷). In JS files you can import these types for reference.You could simply define all the types with TypeScript syntax in *.ts files and just import those types into the JS files.
The thing is in back-end code there is little incentive to use TS JSDoc (just stick to writing a JavaScript with types TS flavour to be on the safe side). It can be useful when you identify code on the hot path that is less than optimal (due to the TS transform) allowing you to take direct control over the exact JS runtime code.
Using JS can be a lot more critical on front end packages where either size matters or where the TypeScript syntax creates a barrier to using a certain parser for some niche code analysis tool or bespoke compiler.
This thread of argumentation has really piqued my interest. I am already well aware of
tsserver
's built-in support for JSDoc annotations, but you have convinced me to more seriously consider it as a means to further minimize the TypeScript surface in my project (particularly for very simple type annotations in function parameters). More complex typings would, of course, still remain in.ts
files, but I am quite attracted to the idea of moving more TypeScript syntax into JSDoc instead.This is not to say that TypeScript is evil. I intend to minimize the TypeScript surface to better emphasize the sufficiency of language semantics when it comes to
.js
files—specifically the OCaml-like assertion that "pure type inference is sufficient".ngl TS JSDoc can be a bit painful, especially in the beginning and too painful for many to even stick with it for any length of time, mostly because esbuild sidelined the constant need for
tsc
anyway.That said with TS JSDoc your premise becomes “JavaScript first, then Types”.
I'd say it's only painful when trying to jam a bunch of TypeScript syntax into it. At that point, we may as well just use TypeScript for that, which is syntactically superior over the hacky shoehorning of TypeScript syntax into JavaScript comments. For simpler annotations of function parameters, however, TS JSDoc is fine with me.
You know, this is actually pretty good. Perhaps that is my ultimate end goal. 🤔
Thank you, @peerreynders for the details. So we are kind in the middle of things where there is no definite alternative to TypeScript.
My take from your explanation is, and correct any details if I miss any:
tsc
is king right now in this arena.I guess, then, that fighting over TypeScript at this point in time is of no consequence: There's no alternative. There should be no feud until the day comes where an alternative is available. Then we can be at each others' throats. 😄
In a TS JSDoc project *.d.ts and *.ts play different roles:
tsc
can then be configured to output a *.d.ts for consumers of the projectThere is no feud. Just a case of many TS developers dismissing TS JSDoc out hand mostly based on developer convenience, additional learning curve and confusion about the difference between JSDoc and TS JSDoc.
@peerreynders I think you make some fair points about TS, but your comparison between dialyzer and TS is entirely a reach. First of all, dialyzer gives you some type safety to Erlang but doesn't extend the language, nor does it let you annotate or operate on types (the language already does that). A fairer comparison would be between Liquid Haskell and TS. Because both are extensions to the language, they are not just a "fancy linter" but an actual extension of the language. That is the case because Typescript enhances Javascript not just with type-safety but with the ability to annotate types and also with features that let you catch another whole class of type errors using, for instance, dependent types. But even if there were no dependent types, TS brings the benefits of a fully-fledged type system because that is what TS is, i.e., a programming language with a static type system.
I understand your argument revolves around typescript's main focus, not being type safety but developer convenience. Although it is a valid point, it's a partial truth. Typescript chose to have unsoundness to make its adoption more accessible, but no language is entirely sound. For example, Haskell is unsound concerning FFI and undefined, or C# is unsound concerning null or typecasting. Thus, it is better to talk of soundness for a set of runtime type errors. In that sense, a language can be more type-safe than others, or in other words, it can be more sound than others, but that doesn't mean the lesser one does not have type-safety. So, type-safety is a critical characteristic that Typescript brings to the table. Still, you have to be extra careful when using its unsound aspects (as in other type-safe languages).
Finally, though you bring up some valid points, I think saying TS is not its language (like Rescript, Purescript, ELM, etc...) is missing the main point of why one should use (TS, Hegel, and Flow) and their difference to JSDocs or a linter.
UPDATE
To be fair, I am being a bit hyperbolic about dialyzer and TS; I think those tools are comparable regarding type checking, although they serve different purposes. But the main point is that a linter does not extend the capability of the language, which is why not every type-checker is a linter.
It was made to reinforce the idea of using TS tooling for the purpose of static analysis in reference to this tweet, i.e. TS-as-a-devtool rather than TS-as-a-language.
In what way does TypeScript extend the capability of JavaScript? JavaScript can be used in ways that can't be statically typed in TypeScript (the point of The Trouble with TypeScript). TypeScript may be a superset to JavaScript in terms of developer features but TypeScript can only express a subset of all valid JavaScript code.
The discussion was around the primarily value proposition behind the TS tooling ecosystem. In particular:
Leveraging TS language features that aren't (soon becoming) standard in JS can easily make portions of that ecosystem inaccessible. The example I chose:
Using decorators would mean foregoing
esbuild
(and therefore Vite) altogether or essentially crippling that development setup:“The solution was to get a plugin that negated all of the performance gained by ignoring TypeScript. The plugin runs each typescript file through
tsc
before passing it toesbuild
.”Once one accepts that the primary value of TS comes from adding types to JavaScript the next step is to acknowledge that the same benefit can be derived (albeit with a bit more pain) by simply using JavaScript and augmenting it with TS types via JSDoc (which is different from JSDoc for documentation).
In the past few months a lot of the "you can't do this with TS JSDoc" statements have been proven wrong even though some of the solutions are ugly or downright flaky.
Preact switched to TS JSDoc ~2020 and SvelteKit in May 2023; so even if it's somewhat tedious it must cover all the bases.
It is this approach of using TS-as-a-devtool (rather than TS-as-a-language) that describes using TS as a "type linter" or more formally as a static analysis tool,
It seems that people cant get through their thick skull that Typescript does a lot of type inference and that nobody ever said that you should be ridiculously verbose:
Typescript !== Java.
I agree! Type inference is so powerful that in many cases,
.js
is surprisingly sufficient for type safety.There is no type safety in JavaScript. JsDoc comments and
.d.ts
files are still typescript.Well, yes. But what I mean is that leveraging TypeScript-powered imports in plain old
.js
files can be sufficient for type safety (because of TypeScript's type inference). I didn't mean to say that JavaScript alone is type-safe.Ok, then I agree, although I still don't see a downside to using .ts as file extension 😜
A week ago I stumbles over this article called Turbo 8 is dropping TypeScript
I don't know Turbo 8, it's the reasoning of the writer that caught my attention. This section made me chuckle:
Reliable. We all experimented with TypeScript at some point. "Maybe it makes sense?" And then, after 3 hours, you realize you could have done it in 5 minutes with ES6.
For big projects and teams, it makes sense. For smaller projects/teams, it's an overkill IMHO.
Thanks for the article though. Respect and good luck!
I personally hated typescript, as how I needed to waste more time on fixing types which ultimately don't benefit me. The only thing that seems useful is when a package provides autocomplete and intellisense thanks to types provided too. Hopefully typescript is removed soon! I have been working on reejs (ree.js.org) which helps me to only get warnings on ide without slowing down the time required to compile it & fixing types which would stop me from proceeding to add new features to my app. I transpile ts to js code on the fly, and the process time is barely noticeable on a 12yr old pc.
So, I definitely see the points about not having to transpile the app.
I've also found that typescript can be extremely limiting at times.
I would say the biggest issue I've had with typescript over the years is that it has not really, IMHO, accomplished fixing the problem it set out to fix. At least not as I understand the problem.
I've been developing with JavaScript for decades. Over that time, I've learned that you have to be really disciplined with JavaScript in order to write code that won't bring down your entire team and project over time. In essence, tech debt in JavaScript accrues at a higher percentage rate than many other languages.
In comes Typescript to solve this issue. Unfortunately, in my experience, it hasn't worked. When I've worked in teams with undisciplined developers in Typescript that have been allowed to get away with letting tech debt into the code, I've had even more pain points with Typescript than JavaScript. I guess the main difference would be that I see those problems faster than I do with JavaScript and perhaps that's the good part, since I can address much earlier that the project is going south. In essence, my experience with Typescript is that the interest rate on tech debt in Typescript has been even higher and compounded more frequently than with JavaScript.
It's fine for disciplined developers, but so is JavaScript.
Without that, the peer review process is really the most important thing in a team.
I would argue the other direction, use typescript in your application vs. a library. I reason that if your library could be used by a js file then you need to build all the type checking into your library since it won't be validated at build time otherwise.
Using it in the app allows for quicker refactoring across the app, which is one of the best things and (I feel) is the main selling point of ts.
The main question I ask when someone wants to use ts vs js is 'What problem are you solving?' - then 'How much does this problem cost you?' - and lastly 'How much do you think going to ts is going to cost you?'
After those three you should be able to decide if you want ts. The problem is most of the time I see teams go to ts, because, well 'types' and I like types :)
Interesting direction you went with here. I have never considered that perspective. Personally, I'd still prefer TypeScript for library code just to avoid the performance hit from frequent runtime type validation.
Instead, I would use something like Zod at a centralized utility module to validate schemas during deserialization time. Then, I would use TypeScript for the rest of the library code under the assumption that the types are valid even in runtime (since I would be enforcing that all deserialization logic passes through the validation layer anyway).
My perspective on this, that either you use JSDoc or ts files its fine if it works for a specific project, both do type checking with typescript anyway.
But going back to just working with javascript for web applications is long term not sustainable, its a pain coming to a codebase that has plain JS (at least for me after I've worked with projects with typescript).
Regarding this, I'd like to reiterate one of my comments from another thread here.
I could be wrong but saying that you only need TypeScript partially in a project or the saying of using TypeScript led to poor developet experience, basically sounds like a red flag to me that those are the kind of people who don't understand the value and benefit of TypeScript and they have been forcing themselves to use it for no good reasons. You should only use it when you think it does what you need. It's either you go big or go home. You always have the choice to use whatever tools you need.
I'd like to clarify my position that I'm not advocating for the removal of TypeScript in a project. I very much rely on its type safety guarantees to enforce compile-time correctness for my applications (to some extend).
What I have presented in this article, however, is the usage of the
.js
extension in cases where full reliance on type inference is sufficient—in which case TypeScript-exclusive syntax is unnecessary. Now this.js
phenomenon is typically the case for application code, which imports the heavily typed library code written in TypeScript (i.e.,.ts
). In my experience, the explicit separation of the.js
and.ts
files thus offers clearer semantics between application/consumer code and library code, which ultimately makes it easier to navigate a codebase.I would highly encourage everyone to listen to this episode of JS Party with Rich Harris (creator of Svelte). They do a really good dive into the “war” between TS people and non-TS people, as well as the recent activity going on with TS and take very fair points for both sides. Extremely valuable information and stuff worth considering for all of us! 😀
Rich also touches on when TS is the “right” tool, and when it’s not.
Yeah, TypeScript versus JavaScript or any other language is very much a matter of preference, and/or team norms.
I know there are a lot of folks who assert a material difference, or warn of the dangers of living without static type validation. Those folks are expressing a personal or team preference, not any actual objective fact.
There are times when you need a type system, such as large-volume data processing in a performance-critical application, and those times are poorly served by a type system that stops at static analysis.
I encourage you to explore and learn however you find it helpful. I can’t wait to see what you do with the skills you build.