DEV Community

Cover image for JavaScript First, Then TypeScript
Basti Ortiz
Basti Ortiz

Posted on

JavaScript First, Then TypeScript

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.

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;
}
Enter fullscreen mode Exit fullscreen mode
// main.ts
import { add } from './add.ts';
console.log(add(1, 2)); // 3
Enter fullscreen mode Exit fullscreen mode

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.


  1. This includes all sorts of transpiling, compiling, tree-shaking, code-splitting, bundling, etc. 

  2. In fact, I am a huge fan of the type-safety conveniences and guarantees that TypeScript enables. 

  3. Even without the TypeScript syntax, the benefits of type safety also extends to JSDoc annotations (which are powered by TypeScript analysis anyway). 

Latest comments (68)

Collapse
 
jefflindholm profile image
Jeff Lindholm • Edited

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 :)

Collapse
 
somedood profile image
Basti Ortiz

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).

Collapse
 
akostadinov profile image
Aleksandar Kostadinov

When an app becomes sufficiently complex, type checking becomes extremely helpful. If you write just a standalone page, perhaps it is less useful. But I would trade safety of types and not getting unexpected data for minor convenience any time.

Collapse
 
somedood profile image
Basti Ortiz

I must reiterate one of my comments from another thread.

I want to be clear here that I'm not advocating for the outright removal of TypeScript (and types) from the codebase. My preference for the .js file is motivated by the fact that much of the application code that I deal with on a daily basis is often the subset of TypeScript that is literally just plain old JavaScript. The .js file extension is therefore syntactically sufficient for most application code. In this case, type safety solely hinges on tsserver's type inference.

Again, I do not argue to remove type safety. I am arguing for the use of a .js file where TypeScript-powered type inference is sufficient.

Collapse
 
akostadinov profile image
Aleksandar Kostadinov

I don't understand why should one be thinking over it instead of just using one thing everywhere.. I can see the purist point of view. For me though it's a negligible concern that would be a nuisance in the long run whenever a ts feature would need to be added to the file. Also inexperienced devs will surely apply it incorrectly - the distinction between app and lib code that you propose.

Thread Thread
 
somedood profile image
Basti Ortiz

I suppose so, but in my experience, I have found it easier to navigate the codebase when the distinction between entry points, consumer code, and library code is clear from the file extension alone. Bonus points for those using code editors that have fancy file icons.

Thread Thread
 
akostadinov profile image
Aleksandar Kostadinov

Maybe if you have created your own convention it works for you. I doubt it can work with random and especially inexperienced devs. Sounds to me though that you may achieve the same or better by a good project structure.

Thread Thread
 
somedood profile image
Basti Ortiz • Edited

I'd like to note that the convention I am proposing seeks to augment an already existing project structure (regardless of whether you may consider it "good"). The discussion on a "good project structure" is irrelevant here (and also beyond the scope of my article) as I only intend to add new semantics on file extensions, not impose a specific project structure (aside from the separation of library code from application code which is arguably a best practice anyway).

New devs can pick up on the existing project structure. The extra file extension semantics are ideally just a cherry on top (that may or may not guide the further refactoring of library code).

Collapse
 
cstroliadavis profile image
Chris

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.

Collapse
 
mickmister profile image
Michael Kochell

If you write any functions, adding types to the parameters makes the code way safer. If you're not writing any functions (doubtful) then you can get by with type inference of imports. Is a function in main.js considered "library code"?

Collapse
 
somedood profile image
Basti Ortiz

Perhaps it could be. If type inference is sufficient (i.e., zero input parameters) or if one settles on JSDoc instead, then that function need not be removed from main.js. But if the typing of the function becomes a little bit too complex, then perhaps it is time to consider upgrading it to a .ts file as "library code".

Collapse
 
brense profile image
Rense Bakker

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:

function add(a: number, b:number):number {
  const total:number = a+b
  return total
}
Enter fullscreen mode Exit fullscreen mode

Typescript !== Java.

Collapse
 
somedood profile image
Basti Ortiz

I agree! Type inference is so powerful that in many cases, .js is surprisingly sufficient for type safety.

Collapse
 
brense profile image
Rense Bakker

There is no type safety in JavaScript. JsDoc comments and .d.ts files are still typescript.

Thread Thread
 
somedood profile image
Basti Ortiz • Edited

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.

Thread Thread
 
brense profile image
Rense Bakker

Ok, then I agree, although I still don't see a downside to using .ts as file extension 😜

Collapse
 
dsaga profile image
Dusan Petkovic

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).

Collapse
 
somedood profile image
Basti Ortiz

Regarding this, I'd like to reiterate one of my comments from another thread here.

I want to be clear here that I'm not advocating for the outright removal of TypeScript (and types) from the codebase. My preference for the .js file is motivated by the fact that much of the application code that I deal with on a daily basis is often the subset of TypeScript that is literally just plain old JavaScript. The .js file extension is therefore syntactically sufficient for most application code. In this case, type safety solely hinges on tsserver's type inference.

Collapse
 
infodsagar profile image
Sagar Dobariya

Let’s limit the use of JS to scripting only to start with. To fix one problem created thousands other.

Collapse
 
latobibor profile image
András Tóth

Javascript first: absolutely agree, when you learn the language. After that TypeScript unless you need to write code that has to run in a console or a throwaway node.js one time script.

The way you must think of TypeScript is this is a productivity tool for your IDE. You write TS, because you want to know where and how things are going to be used. Try renaming a function in a JS codebase over a TS codebase.

For TypeScript to work super effectively you have to always look at the code you write from the perspective of the user of the code and the IDE: it provides clarity to each and relative safety.

When I do data transformation I rely extremely heavily on TS telling me what is my input and what is going to be the output. I can refactor my code while I have those in place. I cannot tell you how frustrating is to work with JS code and not knowing the exact shape of an object. How I need to double-triple check and infer what the gosh darn duck that silly object has or has not. I don't want to work with large JS code bases, because they halve my productivity. Time wasted on checks that can be done by machines.

Lastly there are two evil operators for which any holy warrior must resist temptation: as and any. Usually people cover their lack of understanding with those keywords and make TypeScript lie. And then they blame TypeScript being bad.

Collapse
 
somedood profile image
Basti Ortiz

I don't want to work with large JS code bases, because they halve my productivity.

I want to be clear here that I'm not advocating for the outright removal of TypeScript (and types) from the codebase. My preference for the .js file is motivated by the fact that much of the application code that I deal with on a daily basis is often the subset of TypeScript that is literally just plain old JavaScript. The .js file extension is therefore syntactically sufficient for most application code. In this case, type safety solely hinges on tsserver's type inference.

Collapse
 
motss profile image
Rong Sen Ng

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.

Collapse
 
somedood profile image
Basti Ortiz

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.

Collapse
 
wraith profile image
Jake Lundberg • Edited

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.