How We Actually Use TypeScript on Large Projects
The first sign your large TypeScript project is in trouble isn't a production bug. It's the any count. You run your linter and see hundreds, maybe thousands, of explicit any types creeping into the codebase like weeds. Someone on your team will say, "I'll fix it later," but "later" never comes. These are the TypeScript best practices we’ve had to learn the hard way to keep our projects maintainable, especially as they scale past a handful of engineers.
Ditch any, Embrace unknown, and Get Strict
Look, we've all done it. You're fighting with a complex type from a third-party library, the clock is ticking, and you just type any to make the red squiggles go away. This is tech debt with a high interest rate. Using any effectively deletes TypeScript from that part of your code. You lose autocompletion, you lose type safety, and you open the door to runtime errors that should have been caught at compile time.
Stop it.
The safer alternative is unknown. Think of unknown as a type-safe any. When you have a value of type unknown, you can't do anything with it until you prove what it is. You have to perform some kind of type-checking, like using typeof, instanceof, or a type guard function. This forces you to handle the uncertainty explicitly, right then and there, instead of passing a ticking time bomb through your system.
Your first step in any new project should be enabling strict mode. In an existing one? Your first refactoring task is to get there.
Your tsconfig.json Is Your Constitution
Your tsconfig.json file isn't just a place to specify an output directory. It's the foundational document that defines the rules of engagement for your entire codebase. If it's too loose, your code will be chaos. If it's too strict from day one on a legacy project, you'll create a month-long refactoring nightmare nobody wants to touch.
Start with "strict": true. This one flag turns on a whole family of checks (noImplicitAny, strictNullChecks, etc.) that form the bedrock of a healthy codebase. strictNullChecks alone will save you from countless "cannot read property 'x' of undefined" errors. It forces you to acknowledge that a value could be null or undefined, which is just reflecting reality.
Here's the honest caveat: flipping "strict": true on a 100,000-line JavaScript codebase you've just migrated is a recipe for despair. You'll be staring at 5,000 new TypeScript errors. The pragmatic approach is incremental. Start by enabling noImplicitAny. Fix those errors. Then, turn on strictNullChecks. Work your way through the list. It’s better to have a partially strict codebase that’s improving than a perfectly configured one that nobody can merge code into.
Also, add "noUncheckedIndexedAccess": true. This makes accessing array elements or object properties by an index safer by adding | undefined to the resulting type. It's a small change that reflects how JavaScript actually works.
Let Tools Enforce Your Rules, Not PR Comments
Consistency is key in a large project, but arguing about code style in pull requests is a massive waste of time and energy. Automate everything you possibly can. Your goal should be that code is formatted and linted correctly before it ever gets pushed.
This is a two-part setup:
- Prettier: For code formatting. It's opinionated, which is exactly what you want. Install it, configure a
.prettierrcfile with your team's (very few) preferences, and set it up to run on save in your editor and as a pre-commit hook withhuskyandlint-staged. The debate about semicolons or trailing commas should happen once, be written into a config file, and never spoken of again. - ESLint: For code quality and style rules that go beyond formatting. The
@typescript-eslint/eslint-pluginis non-negotiable. It provides TypeScript-specific rules that are critical for a large project.
Here are a few ESLint rules we enforce religiously:
@typescript-eslint/no-explicit-any: This flags every explicit use ofany. You can useeslint-disable-next-linewith a comment explaining why you need it in rare cases, which is much better than letting it happen silently.@typescript-eslint/consistent-type-imports: This forces you to useimport type { MyType } from './types'when you're only importing types. This seems small, but it prevents circular dependency issues with your bundler and makes the code's intent clearer.@typescript-eslint/no-floating-promises: Ensures every Promise is handled with anawaitor a.catch(). Unhandled promise rejections are a common source of silent failures.
This setup means the feedback loop is immediate. A developer sees the linting error in their editor, not two days later from a frustrated colleague in a PR comment.
Validate Your Boundaries with Schema Libraries
TypeScript is fantastic, but its types disappear at runtime. This creates a major vulnerability at the boundaries of your application—API responses, user input from a form, data from localStorage. You can write a TypeScript interface that describes an API response, but nothing guarantees the actual API will send data matching that shape.
This is where schema validation libraries like Zod, Yup, or Joi come in. We've standardized on Zod, and it's been a clear win.
Instead of defining a separate interface, you define a Zod schema.
const UserSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email() });
You get two things from this one definition. First, you get a runtime validator. When you fetch data from an API, you run it through UserSchema.parse(data). If the data doesn't match the schema—if id is missing or email is not a valid email address—it throws a detailed error immediately. You catch the problem at the source.
Second, you get a static TypeScript type for free. You can just write type User = z.infer<typeof UserSchema>;. Now you have a User type that is guaranteed to match the runtime validation logic. You've created a single source of truth for both your static types and your runtime checks. This is how you build resilient systems.
When you're in a system design interview for a front-end or full-stack role and you're discussing the contract between your service and an external API, mentioning this pattern shows you think about practical failure modes, not just the happy path. It demonstrates a level of seniority that goes beyond just writing type annotations.
Ready to Ace Your Next Interview?
Practice with AI-powered mock interviews tailored to your target role and company. Start Practicing for Free | Explore Interview Prep
