Skip to content

The Three-Year Dependency That Promised More Than It Kept

TSLint promised a stable API for years, then its own maintainers deprecated it. What the real migration bill reveals about catching this risk earlier.

· · 7 min read
Dim archive hallway lined with old shelves, lit only by a single doorway

The README that says it out loud

Open the TSLint repository on GitHub today and the second line of the README does the whole job for you: “TSLint is deprecated.” No hedging, no “in maintenance mode,” no “still supported for critical fixes.” Just a flat statement, a link to an issue titled “Roadmap: TSLint to ESLint,” and a repo that was archived by its owner on March 25, 2021. Read-only. Done.

That bluntness is rare. Most abandoned tools fade out through silence: a changelog that stops updating, issues piling up unanswered, a maintainer who stops replying without ever announcing anything. TSLint’s death was announced, dated, and explained by the people who built it, which makes it a genuinely useful case study, since you don’t have to guess at the timeline.

Here’s the shape of the story, and why it matters for anyone deciding what to add to package.json this quarter.

Why everyone bet on it

Palantir built TSLint in 2013 to lint TypeScript, at a point when ESLint had no idea TypeScript syntax existed. For years that was the entire pitch, and it worked. Angular’s CLI generated a tslint.json file for every new project by default. If you were writing TypeScript in a serious codebase in 2016 or 2017, TSLint wasn’t a choice you made so much as a default you inherited.

That’s the profile this whole site keeps circling back to: a project survives long enough, gets embedded in enough starter templates and CI pipelines, and teams stop treating “will this still be here in five years” as a live question. Why would they? It’s the official thing. Everyone uses it. The docs say it’s stable.

Except “everyone uses it” was never the same claim as “this will outlive your next migration.”

What actually happened

In February 2019, Palantir opened an issue on the TSLint repo laying out a plan to “deprecate TSLint in 2019 and support the migration to ESLint as the standard linter for both TypeScript and JavaScript.” The reasoning wasn’t a scandal or a funding collapse. It was almost mundane. ESLint had gained a TypeScript parser. Once that existed, TSLint’s core advantage, being the only linter that understood TypeScript syntax at all, stopped being an advantage and started looking like duplicated effort. Palantir’s own framing: they used Prettier internally and considered it “a better tool for the job” than a linter also trying to own code formatting.

The TypeScript team itself then migrated its own compiler codebase off TSLint and onto typescript-eslint, becoming a public supporter of the new tooling. When the team that owns the language you’re linting switches horses, that’s not a rumor. That’s the horse race ending.

Is that a betrayal of the teams who’d built their whole toolchain around TSLint? I don’t think “betrayal” is the right word, honestly. Nobody promised eternal support; nobody ever does, explicitly. But “nobody promised it” and “everybody assumed it” can both be true on the same dependency at the same time, and that gap is where the cost lands.

Stack of brown cardboard boxes on a wooden table, ready to be packed
Photo by Brett Jordan on Unsplash

The migration bill nobody priced in

Here’s where it gets concrete, because the typescript-eslint team didn’t just announce a replacement and walk away. They shipped tslint-to-eslint-config, an official tool that reads your existing tslint.json, your tsconfig.json, and your package configuration, then generates an .eslintrc.js that maps as many rules as it can to their ESLint equivalents. For rules with no direct ESLint match, it wraps them with eslint-plugin-tslint instead of just dropping them silently. That’s a genuinely thoughtful piece of migration tooling, and it’s free.

It’s also not the whole job, and pretending otherwise is exactly the trap. Three costs the automated tool doesn’t touch:

Every inline suppression comment in the codebase, all your // tslint:disable and // tslint:disable-next-line lines, needs converting to ESLint’s // eslint-disable syntax by hand or with a separate script. On a codebase old enough to have accumulated three years of TSLint suppressions, that’s not a rounding error.

Angular’s own community tooling had a schematic for this, ng g @angular-eslint/schematics:convert-tslint-to-eslint, and its migration guide is refreshingly honest about the ceiling: “there is a lot of work to do to ensure a smooth transition to the new tooling without any regressions.” Several Codelyzer-specific rules, no-unused-css and prefer-inline-decorator among them, simply have no ESLint equivalent at all, so teams either dropped that coverage or hand-rolled a replacement. Here’s the twist that should sting: angular-eslint removed the conversion schematic entirely as of version 16. The assisted path had a shorter shelf life than the migration window some teams actually took.

That quote isn’t invented. Palantir’s own 2019 announcement said outright that “there are features, test suites, and conveniences in TSLint which we hope to retain in the migration.” Hope. Not “will.” A team that reads its own migration path carefully enough to catch that word choice gets a head start that a team skimming the headline never does.

An archived repo is not the same thing as maintenance mode

It’s worth separating two states that get talked about as if they’re interchangeable. A project in maintenance mode, moment.js is the textbook example, still accepts security patches and still recommends against starting new projects with it. TSLint isn’t that. TSLint is archived: read-only, no new commits accepted from anyone, ever. If a security issue turns up in TSLint’s dependency tree tomorrow, nobody is patching it. That’s not a hypothetical for teams who still have a tslint.json sitting in a legacy service.

I’ve had to migrate off dependencies more than once where the internal argument was “it still works, why touch it,” and the honest answer is that “still works” and “still maintained” measure completely different things. One is a snapshot. The other is a promise about the future, and TSLint’s own maintainers are the ones who told you, in writing, that the promise had expired.

Close-up of a rusted gray metal chain-link fence
Photo by Chauncey Sims on Unsplash

The signal that would have flagged this three years early

Hindsight makes TSLint’s decline look obvious. It wasn’t, not from inside 2017. So what actually would have been visible early, before the 2019 announcement, to a team doing real dependency-risk assessment instead of just checking “does it still get npm downloads”?

Three things, and none of them require a crystal ball. First, watch what the platform vendor is doing, not just the library. ESLint shipping a real TypeScript parser was the tell. Once the tool one layer down in the stack absorbs the exact capability that gave your linter its reason to exist, the linter’s independence is now a cost with no matching benefit, and somebody on the maintaining team will eventually notice that math even if users don’t. Second, read maintainer language for hedges, not just headlines. “We hope to retain” is a different sentence than “we will retain,” and the difference is the whole risk. Third, and this is the one I’d actually treat as a hard trigger rather than a soft signal: when the organization that built the language or runtime your tool depends on starts building its own competing integration, that’s not a rumor to watch, that’s a countdown that already started.

None of those three needed 2019 hindsight. All three were visible from a changelog and a couple of GitHub issues, sometime around 2017 or 2018, to anyone actually looking instead of assuming the default choice would stay the default forever.

What this means for the next TSLint sitting in your stack

I’m not going to pretend every three-year-old dependency is secretly doomed; most aren’t, and treating every stable tool as a ticking clock is its own kind of paralysis. But the pattern here, wide adoption plus platform-vendor overlap plus hedged maintainer language, is checkable in about twenty minutes for anything load-bearing in your build. Run it against your own package.json before you run it against someone else’s postmortem. For the companion piece on tools that never got a formal deprecation announcement and just quietly stopped existing in people’s stacks, see What Disappeared From the Web Ecosystem in 20 Years, and Why.