I resisted TypeScript for years with the same argument I heard from most holdouts, that I already knew what type a variable was supposed to be, and adding a type system on top felt like ceremony for a problem I didn't personally have. A new job required it, no choice involved, and six months in I want to be honest about which parts of my skepticism turned out to be right and which parts I was simply wrong about.
For small, personal scripts I write and never touch again, TypeScript genuinely does add overhead that doesn't pay for itself, defining interfaces for a fifty-line script I'll run once feels exactly like the ceremony I always complained about, and I still reach for plain JavaScript for that specific category of throwaway code without any guilt.
Three weeks into the new job, I refactored a function that used to accept a plain object and changed it to accept an array instead, a change I made confidently because I'd traced through the handful of callers I remembered. TypeScript's compiler immediately flagged four other call sites I'd completely forgotten existed, each of which would have broken silently in JavaScript and only surfaced as a runtime error days later, probably in production, probably reported by a confused user rather than caught by me.
// before
function processOrder(order: { id: string; items: Item[] }) { ... }
// after refactor
function processOrder(orders: { id: string; items: Item[] }[]) { ... }
// TypeScript immediately flagged every caller still passing a single object
Basic type annotations on function parameters and return values felt intuitive within days. Generics took genuinely longer, understanding why function identity<T>(arg: T): T is more useful than typing the argument as any didn't click until I hit a real situation, writing a reusable data-fetching hook that needed to work correctly across several different API response shapes without losing type safety on the way through.
function useFetch(url: string): { data: T | null; loading: boolean } {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
// ...
return { data, loading };
}
// usage: const { data } = useFetch('/api/products/1');
// data is correctly typed as Product | null, not any
Our tsconfig originally had strict: false because the migration from an existing JavaScript codebase felt overwhelming otherwise, and I'll admit that config choice let me write plenty of code with implicit any types that provided essentially none of TypeScript's actual safety benefit while still paying its syntax cost. Turning on strict mode for new files specifically, while grandfathering older files, gave us the real benefit going forward without a full-codebase rewrite nobody had time for.
The type checking itself catching bugs before runtime is the headline benefit everyone talks about, but the thing that's actually changed my daily coding speed the most is autocomplete knowing exactly what properties and methods are available on a given object, without me needing to console.log something to remember its shape, a habit I did constantly in plain JavaScript and almost never do anymore.
Working with a specific third-party library that had incomplete or outdated type definitions, I spent real time fighting the type system to describe a shape it didn't already know about, writing manual type declarations for a library that should have shipped its own. This is a genuine cost, not every library's TypeScript support is equally mature, and that inconsistency is a real, ongoing frustration rather than a one-time learning curve.
The refactor that quietly broke four callers is the exact scenario I'd have dismissed as unlikely before actually experiencing it, and it's the single piece of evidence that's genuinely changed my mind. I was right that TypeScript adds real overhead for small throwaway scripts. I was wrong that my own mental model of a codebase was reliable enough to skip a compiler that checks it for me, especially once a codebase gets large enough that no single person holds the whole thing in their head anymore.