Actually, I should clarify — I used to waste hours of my life in code reviews arguing about semicolons. Literally hours. “Hey, you missed a space after that bracket.” “Actually, I prefer 2 spaces, not 4.” It was miserable. But if you’re still doing that in 2026, you are probably wasting your company’s money and your own sanity.
We solved this years ago with Prettier, but setting it up with TypeScript can still be a headache, especially since the ESLint “Flat Config” migration broke half the tutorials on the internet last year. I spent last Tuesday fixing a broken CI pipeline because a junior dev (bless him) tried to manually format a 500-line interface file. It didn’t go well.
And here is how I actually set up Prettier with TypeScript today. No fluff, just the config that stops the arguments.
The Setup That Actually Works
First off, stop trying to make ESLint do formatting. I see this all the time — people install fifty plugins to make ESLint behave like Prettier. But just don’t. ESLint is for finding bugs; Prettier is for making code look pretty. Keep them separate.
I’m currently running this setup on a Node 22 backend service, and it’s rock solid:
npm install --save-dev prettier eslint-config-prettier
That’s it for the heavy lifting. The eslint-config-prettier package turns off all the ESLint rules that conflict with Prettier. If you’re using the new ESLint flat config (eslint.config.js), it looks like this:
// eslint.config.js
import prettier from "eslint-config-prettier";
export default [
// ... your other typescript configs
prettier, // Put this last to override everything else
];
My .prettierrc file? It’s boring. And boring is good. I keep this in the root of every project I touch:
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
The trailingComma: "all" setting is non-negotiable for me. It makes git diffs so much cleaner when you add a new property to an object or a new argument to a function. You don’t see that annoying “modified line” just because you added a comma.
Handling Ugly TypeScript Generics
TypeScript can get verbose. Like, really verbose. When you have complex generics, manually formatting them is a nightmare. Prettier handles this surprisingly well, breaking long types onto multiple lines so they’re actually readable.
Here’s a real-world example. I was working on a generic API handler recently. Before Prettier, it was a mess of horizontal scrolling. After hitting save, it snapped into this:
interface ApiResponse<T> {
data: T;
status: number;
timestamp: string;
error?: {
code: string;
message: string;
details?: Record<string, unknown>;
};
}
// An async function demonstrating API calls with strict typing
async function fetchData<T>(
url: string,
retries: number = 3
): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const data = (await response.json()) as T;
return {
data,
status: response.status,
timestamp: new Date().toISOString(),
};
} catch (error) {
if (retries > 0) {
console.warn(Retrying... attempts left: ${retries});
return fetchData<T>(url, retries - 1);
}
throw error;
}
}
Notice how it handles the indentation on the return object? If I had typed that out on one line, Prettier would have exploded it out exactly like this because of the printWidth: 100 setting. It saves me from having to hit “Enter” five times manually.
The DOM and Type Casting
Another place where formatting gets tricky in TypeScript is when you’re dealing with DOM elements and type assertions. You end up with these long lines of as HTMLInputElement. I ran into this building a simple form validator last week.
Here is how Prettier keeps the logic clear even when the types get noisy:
function setupFormListeners(formId: string): void {
const form = document.getElementById(formId) as HTMLFormElement | null;
if (!form) {
console.error(Form with ID "${formId}" not found);
return;
}
form.addEventListener('submit', (event: Event) => {
event.preventDefault();
// Prettier keeps these assertions readable
const emailInput = form.querySelector(
'input[name="email"]'
) as HTMLInputElement;
const passwordInput = form.querySelector(
'input[name="password"]'
) as HTMLInputElement;
if (emailInput?.value && passwordInput?.value) {
submitLogin(emailInput.value, passwordInput.value);
}
});
}
function submitLogin(email: string, pass: string) {
console.log(Logging in ${email}...);
}
See that const emailInput block? Prettier automatically wrapped the query selector arguments because the line was getting too long with the type assertion. It’s a small thing, but when you’re scanning code at 4 PM on a Friday, readability matters.
My “Secret” Weapon: Import Sorting
Here is where I get a bit opinionated. I insist on using prettier-plugin-organize-imports.
Some people say, “Just let VS Code handle it on save.” But here is the problem with that: VS Code settings are local. If my coworker opens the project in Vim or WebStorm, or if they just didn’t copy my settings.json, they commit messy imports. Then I pull their code, hit save, and boom—merge conflict because my editor rearranged 20 lines of imports.
By putting the sorting logic into Prettier, it happens in the CI pipeline. It happens for everyone, regardless of their editor.
The Benchmark: I actually tested this on our main repo (about 150k lines of code) last month. I was worried the plugin would slow down the formatting process.
- Without plugin: Prettier ran in 1.8 seconds.
- With organize-imports plugin: Prettier ran in 2.4 seconds.
It adds about 600ms overhead for the whole project. But that is absolutely worth it to never see a duplicate import or a chaotic import list again. It also automatically removes unused imports, which is a nice side effect for keeping bundle sizes down.
Integration with Husky (Do it.)
If you don’t automate this, people will forget. I forget. I’m the one writing the article and I still forget to format sometimes. So I use Husky with lint-staged to run Prettier only on the files that changed.
In your package.json:
"lint-staged": {
"**/*.{ts,tsx,js,jsx,json}": [
"prettier --write"
]
}
This saved my bacon just yesterday. I had pasted some JSON config from a Slack message (which always messes up the quotes) and tried to commit it. Husky caught it, formatted it, and committed the clean version. Zero effort.
Look, the tools are there. TypeScript 5.7 is great, Prettier is fast. There is no reason to be manually hitting the spacebar anymore. Set up the config, add the pre-commit hook, and get back to actually building stuff.
