TSLint is dead

Klaus Meinhardt
Jul 26, 2018 · 6 min read

… or at least it smells like it.

First things first: I’m not a maintainer of TSLint and not affiliated with Palantir, the company behind the tool. Opinions are my own, but feel free to adopt them.

My GitHub handle is @ajafff and I’m building awesomeness in the TypeScript ecosystem.

Lack of maintainers

When I started contributing to TSLint in late 2016, there were at least 2 active maintainers. Review cycles took a couple of days at most and reviewers knew quite a bit about the code they were maintaining.

That latest until June 2017 when all the sudden nobody was there to manage issues and pull requests. This lead me to open this issue asking about becoming a collaborator: https://github.com/palantir/tslint/issues/2985.

Being a collaborator helped at first. Just a few weeks later my pull requests started to pile up again. They got merged without a thorough review in occasional bursts of activity from maintainers. It took me until November 2017 to finally lose my motivation and realise it’s not going the get better anytime soon.

And I was right. From January to April 2018 there were no commits at all, no pull request reviews and no resolved issues. IMO this is really bad for a big company’s reputation in the open source community. I wonder if they ever though about open source as a tool to promote themselves to talented developers.

Lack of experience

The new maintainers, who where put in charge in early April 2018, started out carefully by reviewing and landing only the simple pull requests. Unfortunately it seems there wasn’t any experience built up since then. There’s just no other explanation why some of the more recent changes made it into the code.

Let’s start with a simple rule and a seemingly innocent change that can lead to totally invalid code: https://github.com/palantir/tslint/pull/4029. Try quotemark‘s new option backtick on the following code and you will get parse and compile errors all over the place:

Long standing design and architectural issues

Besides the aforementioned issue, there are known issues and limitations in the runtime that prevent further innovation. These require a lot of effort, knowledge about the whole codebase and quite a few breaking API changes.

This list goes from dangerous bugs/features to confusing configuration to must have features that are currently impossible to implement.

Overlapping fixes

Other than the quotemark example above, which is clearly a bug in the implementation, there is an issue when multiple failures advertise correct fixes that overlap.

I initially reported this in April 2017: https://github.com/palantir/tslint/issues/2556. Nothing has changed since then and yet changes like https://github.com/palantir/tslint/pull/4066 are merged.

Potentially dangerous fixes

https://github.com/palantir/tslint/issues/2625 discusses whether fixes are allowed to change the semantics of the code. The conclusion was to add a new option --fix-unsafe to explicitly opt into unsafe fixes.

One such fix has long been part of TSLint: no-var-keyword. Another one was repeatedly proposed for triple-equals. It’s unfortunate that https://github.com/palantir/tslint/pull/3935 has landed with another potentially dangerous fix for member-ordering even though the proposed new option was not yet implemented. As it stands--fix always includes unsafe fixes.

Fixing files with syntax errors

Syntax errors can break lint rules in many unexpected ways. But TSLint treats all failures and their fixes as valid and applies them to the code. A simple error like forgetting a semicolon or comma can lead to a totally destroyed file.

Rule name collisions

One of my big issues as a maintainer of a custom rule plugin tslint-consistent-codestyle is that all rule names are global. That means I cannot use a name that’s already occupied by a core rule. When TSLint adds a new core rule with the same name as one of my custom rules, all of my users will automatically use the core rule with certainly incorrect configuration. This happened before and caused tslint-eslint-rules to rename and prefix almost all of their rules: https://github.com/buzinas/tslint-eslint-rules/issues/129.

Using multiple plugins makes that export a rule with the same name is a bit like gambling. The precedence is determined by the order of extends or rulesDirectory. When mixing both, you need to know about the inner workings of TSLint to know which one wins.

“linterOptions” — which isn’t what it says

linterOptions in tslint.json has actually nothing to do with the linter — where linter in TSLint refers to the part that simply executes the rules. Instead this option is only used by the command line executable. This is reported in https://github.com/palantir/tslint/issues/3481 with other issues requesting support for that option in the API https://github.com/palantir/tslint/issues/3881, https://github.com/palantir/tslint/issues/3722.

Not only does this confuse users and implementers, it’s also hard to correct now that it has shipped for such a long time. Even worse, there will be a new option linterOptions.include added in this PR: https://github.com/palantir/tslint/pull/4056.

No place for global configuration

Related to that linterOptions issue described above, there is the need to store all CLI options in a config file. This helps external tools to pick up these options and allow for reproducible executions. https://github.com/palantir/tslint/issues/2227

tslint.json isn’t the right place for such configuration because each file could be linted using a different tslint.json in a single execution of TSLint. But there is just no other config file at the moment. So how to resolve that issue? Stuff everything into linterOptions and make that thing even more confusing?

Lacking support for processors

The one thing Vue.js developers who use TypeScript wish for is native support for Vue Single File Components in TSLint without the need for some build tool like Webpack: https://github.com/palantir/tslint/issues/2099.

It turns out to simply be not possible with the current API layering. This pull requests sums it up very well: https://github.com/palantir/tslint/pull/3596.

Without a major rewrite and a lot of breaking API changes this will not be possible to implement in a way that works.

Lacking support for config overrides

Another big flaw in the configuration is the lack for conditional config overrides, glob config, however you call it: https://github.com/palantir/tslint/issues/3447. ESLint ships this feature for quite some time now, but TSLint is not able to correctly implement it.

This change requires breaking API changes as described in this probably never-to-be-merged pull request https://github.com/palantir/tslint/pull/3708.


Why do I care?

First of all, I want you to write the best code possible. Of course I also demand that from my team mates. That’s the whole reason I started writing lint rules.

It’s sad to see a tool with such a widespread adoption struggle to evolve and overcome its design flaws. TSLint is integrated almost everywhere. Shareable configs allow you to adopt the style of others. Autofixing avoids adding or removing commas and semicolons manually. Autofix on save allow you to fix all fixable issues every time you change a file. But all of that comes with a price: People expect TSLint to fix code style and formatting without changing the meaning of their code. Most of the issues mentioned above do the exact opposite. I don’t feel comfortable any more recommending TSLint to others.

And of course I want to advertise my solution:

The Solution

After quitting TSLint I decided to build my own linter runtime that fixes all of these issues. Fortunately I could learn from the design mistakes made in TSLint to build a highly extensible linter. The whole thing is called Fimbullinter and the linter runtime / executable is called Wotan. If you haven’t noticed yet, the names refer to norse mythology. You can read more about that in the documentation: https://github.com/fimbullinter/wotan#whats-up-with-those-names

That linter comes with a well-chosen set of core rules https://github.com/fimbullinter/wotan/blob/master/packages/mimir/README.md#rules. These rules are designed to avoid any false positives of their TSLint counterpart and never change the semantics of your code.

In addition you can use plugins to add or change the behaviour of the linter runtime.
The Heimdall plugin allows you to use TSLint rules as if they were Fimbullinter rules. This also allows you to work around the issue of global rule names, because everything is prefixed. For more details visit the documentation at https://github.com/fimbullinter/wotan/blob/master/packages/heimdall/README.md.
The Valtyr plugin goes one step further and uses your existing tslint.json (with all of it’s flaws) and adds support for processors, handling of overlapping fixes and many more. In fact Wotan + Valtyr is the better TSLint. As always, more details are in the docs: https://github.com/fimbullinter/wotan/blob/master/packages/valtyr/README.md.

If you are a rule author, you can provide your rules for TSLint and Fimbullinter with minimal effort using Bifröst as seen in https://github.com/ajafff/tslint-consistent-codestyle/commit/e1e6419cdac9f809b430c7ae73a88eee357c6d78.

Caveats

  • There are currently no editor integrations or any other plugins for build tools like webpack (proposal), grunt, gulp, angular-cli (proposal), whatever.
  • The API may change a bit, but big changes are unlikely.
  • A few rule names will probably change until version 1.0.0
  • Documentation is still a little sparse.

If you need any help getting started, please file an issue or drop a message in the Gitter chat.

Klaus Meinhardt

Written by

Building awesomeness: https://github.com/ajafff

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade