I distinctly remember a conversation I had at a meetup back in 2018. Some guy was arguing—loudly—that TypeScript was just “Java for people who failed at JavaScript.” He insisted that the dynamism of JS was a feature, not a bug, and that adding types was just bureaucratic overhead.
I wonder what that guy is doing today.
Because if you look around right now, at the tail end of 2025, the war is over. TypeScript didn’t just win; it dominated. It’s the most used language on GitHub now, finally pushing past Python. And honestly? Thank god. I spent way too many years of my life chasing down undefined is not a function errors at 3 AM because someone passed a string instead of a number to a helper function three layers deep.
But here’s the thing. Just because we’re all writing .ts files doesn’t mean we’re doing it right. I still see codebases that are basically JavaScript with any sprinkled on top like bad seasoning. If we’re going to accept our new typed overlords, we might as well use the tools properly.
The “It Works on My Machine” Problem
The biggest lie we told ourselves about vanilla JavaScript was that it was “faster to write.” Sure, typing function add(a, b) { return a + b } is physically faster than defining types. But you know what’s slow? Debugging why add("5", 10) resulted in "510" in production causing a billing error.
I’ve been working on a legacy migration recently (aren’t we always?), and the difference in cognitive load is absurd. When I open a TS file, I know what the data looks like. I don’t have to console.log the object just to see if the property is userName or username.
Here’s a practical example of where I see people getting lazy. They fetch data and just assume it matches their interface.
// The lazy way (Don't do this)
interface User {
id: number;
email: string;
isActive: boolean;
}
async function getUserBad(id: number): Promise<User> {
const res = await fetch(/api/users/${id});
// This is a lie. You don't know if this is actually a User.
// If the API changes, your app crashes at runtime.
return await res.json() as User;
}
That as User cast is dangerous. It’s you telling the compiler “Shut up, I know what I’m doing.” Usually, we don’t.
In 2025, with tools like Zod being standard in pretty much every stack I touch, there is zero excuse for not validating at the runtime boundary. Here is how I actually write this stuff now. It’s more verbose, yeah. But it doesn’t break.
import { z } from 'zod';
// Define the schema first
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
isActive: z.boolean(),
// Optional fields save you from "cannot read property of undefined"
lastLogin: z.string().datetime().optional()
});
// Derive the type from the schema
type User = z.infer<typeof UserSchema>;
async function getUserSafe(id: number): Promise<User | null> {
try {
const response = await fetch(https://api.example.com/users/${id});
if (!response.ok) {
throw new Error(API Error: ${response.status});
}
const rawData = await response.json();
// This actually checks the data structure at runtime
const parsedUser = UserSchema.parse(rawData);
return parsedUser;
} catch (error) {
console.error("Failed to fetch user:", error);
// Handle the error gracefully instead of crashing the UI
return null;
}
}
// Usage
(async () => {
const user = await getUserSafe(42);
if (user) {
console.log(Emailing ${user.email}...);
}
})();
See the difference? If the API sends back garbage, Zod throws a specific error telling me exactly which field is wrong. I don’t spend three hours debugging a silent failure.
DOM Manipulation Without the Headache
Another area where I see people fighting TypeScript is the DOM. The browser APIs are old, clunky, and return generic types. It’s annoying. You select an element, try to access .value, and TS yells at you because HTMLElement doesn’t have a value. Only HTMLInputElement does.
So people just slap any on it or use @ts-ignore. Please stop.
I used to be guilty of this too, mostly out of frustration. But writing a proper type guard function takes like thirty seconds and saves you so much pain later.
// A reusable type guard
function isInputElement(element: Element | null): element is HTMLInputElement {
return element !== null && element instanceof HTMLInputElement;
}
function setupSearch() {
// TypeScript knows this is generic Element | null
const searchInput = document.querySelector('#main-search');
const searchButton = document.querySelector('#btn-search');
// This check narrows the type automatically
if (!isInputElement(searchInput)) {
console.warn("Search input not found or wrong element type");
return;
}
// Now safe to access .value
searchInput.value = "Default Search Term";
searchInput.addEventListener('input', (e) => {
// Event targets are tricky. Explicit casting is sometimes needed here,
// but checking instance is safer.
const target = e.target;
if (target instanceof HTMLInputElement) {
console.log(Typing: ${target.value.toUpperCase()});
}
});
}
This pattern—narrowing types rather than forcing them—is the mental shift that took me the longest to make. Once it clicks, though, going back to raw JavaScript feels like walking a tightrope without a net.
The Configuration Nightmare
Look, I’m not going to pretend everything is perfect. If I have one major gripe with the state of TypeScript in late 2025, it’s the configuration. tsconfig.json has become a beast. Between moduleResolution strategies, strict modes, and trying to get it to play nice with whatever bundler is popular this week (are we still using Vite? I think so), it’s a mess.
I spent an entire Saturday last month just trying to get a monorepo to share types correctly between a backend and frontend. I nearly threw my laptop out the window. The tooling is powerful, but the learning curve for the infrastructure of TypeScript is way steeper than the language itself.
But that’s the trade-off. We pay the tax upfront in configuration and boilerplate so we don’t pay the tax later in production bugs.
Looking at 2026
So where do we go from here? The “Type Annotations” proposal for JavaScript is still floating around, which would let browsers run TS syntax (ignoring the types) natively. That would be huge for development speed—no compile step for simple scripts. But until that lands, we’re stuck with the build step.
My advice? If you’re still holding out, stop. The industry has spoken. Python falling to #2 isn’t a fluke; it’s a signal that the web ecosystem (and the backend JS ecosystem) has matured. We aren’t just scripting interactions anymore; we’re building massive, complex systems. And you can’t build skyscrapers with duct tape.
Just do me a favor: turn on strict: true and don’t turn it off. It hurts at first, but it’s the good kind of pain.
