What You Need to Know About Consistent TypeScript Code
Most teams I work with have some kind of linting setup, but very few of them actually enforce it in a way that matters. The TypeScript Style Guide isn't a single official document from Microsoft — it's more of a collection of conventions that accumulated over years of people dealing with the same problems. Things like where to put your semicolons, how to name your interfaces, whether you should use `type` or `interface` for public APIs. It's messy because TypeScript itself is still evolving. I ran into a real headache last year when migrating a codebase from an older version of TypeScript to 5.4. We had about 12,000 type declarations spread across 40 modules, and nearly half of them were using a mix of `interface` and `type` interchangeably. The build itself didn't fail — TypeScript is pretty lenient here — but the team started seeing weird inference issues in a couple of nested generic contexts where the compiler couldn't resolve the type properly. The workaround was to standardize on `interface` for object shapes and `type` exclusively for unions, intersections, and mapped types. Takes about 4 hours across a medium-sized project to audit and fix. Nothing dramatic, but it saves future pain.
TypeScript Style Guide
The core of a TypeScript Style Guide comes down to a handful of decisions that affect how your code reads, how the linter behaves, and how much friction your team experiences day-to-day. These are the ones that matter most. Indentation and semicolons. Use two spaces, not four. Four spaces is fine for small scripts but it adds visual noise quickly in deeply nested generic types. Semicolons should be consistent — either always on or always off. Mixing them causes issues with ASI in edge cases that are genuinely annoying to debug. Set this up in your ESLint config so nobody has to think about it. Interface versus type. This is the most debated topic in the TypeScript community and the answer depends on what you're building. For internal utilities, it doesn't matter much. For a public API that other teams depend on, use `interface`. The reason is that interfaces support declaration merging, which makes them more resilient when multiple libraries augment the same shape. Types with the same name don't merge — they conflict and throw a hard error at compile time. I learned this the hard way when a third-party package silently added an augmentation for a type alias our team was using, and the build started failing in production after a routine `npm install`.
Generic parameter naming. Use single uppercase letters or descriptive names like `TValue`, not `T`. `T` is fine in short utility functions where the context is obvious, but in anything over three lines of type logic, `TValue` or `TKey` saves you from re-reading the signature every time. The convention isn't enforced by the compiler so you need to make it a team norm or add an ESLint rule for it. Unused type imports. This is one of those things that seems trivial but causes real problems. TypeScript compiles without errors when you import a type and never use it in the type position — it just erases it at emit time. But some bundlers and linters flag this, and it clutters the import list. Set `noUnusedLocals` and `noUnusedParameters` in your tsconfig to catch these early.
Get the Full Details

Setting Up the Tooling
You don't need a massive toolchain. Here's what actually works in practice. tsconfig.json settings. Start with `strict: true`. This alone enables `strictNullChecks`, `noImplicitAny`, `strictFunctionTypes`, and several others. Don't disable individual strict flags to make migration easier — migrate incrementally using `skipLibCheck` and `noEmitOnError` instead. Setting `strict: false` on a large project typically creates a debt that takes 2-3 weeks of refactoring to clear later. ESLint with @typescript-eslint. Install `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, and configure them to extend the recommended config. Add the `eslint-config-prettier` package if you use Prettier, otherwise you'll get conflicting rules about formatting. Run `eslint --fix` on your codebase once — it will resolve about 60% of style issues automatically.
Prettier integration. Use it with a `.prettierrc` file. Set `semi: true`, `singleQuote: false`, `tabWidth: 2`, `trailingComma: "es5"`. The es5 trailing comma setting is important — it avoids a TypeScript parsing error that happens with object literals when trailing commas are used in ES3 target mode.
Common Pitfalls That Wasted My Time
The `any` trap. When migrating a JavaScript codebase to TypeScript, the fastest path is often marking things as `any`. This is technically correct from a compiler standpoint — the code will run. But every `any` you leave in is a hidden bug waiting to surface. A realistic number: in my experience, a project with more than 5% `any` annotations will have at least one class of bugs related to incorrect assumptions about runtime shape. The rule of thumb is to replace `any` with `unknown` first, then narrow it with a type guard. This adds about 10-15% more code in the short term but reduces runtime crashes significantly. Implicit `any` from third-party packages. Some packages ship without type declarations and rely on the `@types` scope. When those are missing, TypeScript falls back to `any` for the entire import. Check your dependency tree for packages missing type definitions — run `npm ls | grep @types` or just look for `node_modules` packages without a `index.d.ts`. Adding `// @ts-ignore` on top of every such import is a shortcut that compounds. Instead, write a thin type wrapper file that declares the expected shape. Overusing intersection types for composition. TypeScript developers often reach for `{ A } & { B }` when they mean to define a new interface. Intersection types create an implicit coupling that makes refactoring painful. If component A changes its property names, every intersection that uses it breaks. An explicit `interface C extends A, B` is easier to read, easier to refactor, and produces better error messages when something goes wrong.

What the Style Guide Can't Fix
No style guide or linter configuration will save you from bad architectural decisions. I've seen teams enforce every stylistic rule perfectly while their type system was fundamentally broken — circular type dependencies, excessively deep generic nesting, and monolithic type definitions that made IDE autocomplete unusable. These are structural problems, not style problems. The fix requires actual restructuring, and no amount of prettier formatting will help with that. Another limitation: style guides don't handle naming conventions well. What looks clean to one developer might be confusing to another. Words like `Model`, `Service`, `Controller`, `Entity`, and `DTO` mean different things across codebases. The best teams I've worked with maintain a simple naming dictionary in the repository root rather than trying to enforce it through tooling.
Practical Migration Steps
If you're starting fresh, configure your tooling before writing any TypeScript. Get the linter, formatter, and strict tsconfig running, then commit the configuration. If you're migrating an existing project, do it module by module. Lock the tsconfig for each module as you convert it — set `"noEmit": false` only for modules that are ready, keep the rest on `"noEmit": true` until they pass. This prevents the build from producing broken JavaScript while you're still adding types. The initial migration of a medium project (roughly 10,000 lines of JavaScript) typically takes one to two weeks for a team of two. The bulk of the time goes to resolving `any` annotations and fixing the occasional circular dependency that the compiler surfaces for the first time. After that, maintaining the style guide is mostly about keeping the ESLint rules updated when TypeScript releases a new version that introduces stricter checks.