Josh Goldberg
The DMV sloth from Zootopia, Flash, slowly breaking into a smile.

Why Typed Linting Needs TypeScript Today

Sep 23, 202415 minute read

Typed linting is powerful but requires a full type checker to function well. Today, that means TypeScript. This is why we haven't found a suitable replacement yet.

Recap: Typed Linting?

Linting with type information, also called “typed linting” or “type-aware linting”, is the act of writing lint rules that use type information to understand your code. Typed linting as provided by typescript-eslint is the most powerful JavaScript/TypeScript linting in common use today. Lint rules that use type information are significantly more capable than traditional, AST-only rules.

👉 For a deeper explanation of typed linting, see the deploy preview of typescript-eslint > Typed Linting: The Most Powerful TypeScript Linting Ever, a work-in-progress blog post.

Many popular lint rules have ended up either dependent on typed linting or having to deal with known bugs or feature gaps without typed linting 1 2. ESLint’s core rules don’t understand type information, leading to some typescript-eslint “extension” rules adding in type information 3.

So, typed linting is really, really important for a linter.

And how do you get type information? Well…

TypeScript For Type Information

TypeScript is the tool for providing full TypeScript type information on JavaScript or TypeScript code. It’s by far the most popular flavor of JavaScript during this era of web development.

In fact, TypeScript is the only tool that can reasonably retrieve type information today. Every public effort to recreate it is either abandoned4 or stalled5. Flow is explicitly not targeting competing with TypeScript for public mindshare6. The closest publicly known effort right now is Ezno, which is very early stage.

If you want a full type checker for your JavaScript/TypeScript project (which, again, you do), then TypeScript is your only reasonable choice today.

Alternatives and Difficulties

TypeScript is a large dependency. Type checking is a notoriously expensive process. TypeScript executes at JavaScript speeds (“JIT”, or Just In Time compiling), and so takes up exponentially more memory and execution time than tooling written in native speed languages such as Go or Rust.

It would be fantastic for linter users if we could find away around having to run the full JIT speed type checker as part of our typed linting. Such a feat has not been successfully implemented yet.

AST-Only Types

One way to avoid a TypeScript dependency could be to support only limited type retrievals: effectively only looking at what’s visible in the AST. I’d wager you could get somewhat far with basic AST checks in a file for many functions, and even further with a basic TypeScript parser that builds up a scope manager for each file and effectively looks up where identifiers are declared.

Sadly, an AST-only type lookup system falls apart fairly quickly in the presence of any complex TypeScript types (e.g. conditional or mapped types). Most larger TypeScript projects end up using complex types somewhere in the stack. Any modern ORM (e.g. Prisma, Supabase) or schema validation library (e.g. Arktype, Zod) employs conditional types and other shenanigans. Not being able to understand those types blocks rules from understanding any code referencing those types.

Inconsistent levels of type-awareness are at best confusing for users. They’re practically a blocker to real adoption of a linter.

A full type system such as TypeScript’s is the only way path to fully working lint rules that perform any go-to-definition or type-dependent logic.

Native Speed Reimplementation

I previously touched on similar points in Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now > Option: Reimplementing TypeScript at Native Speed.

If type checking is so important, and a native speed type checker would be so beneficial, why hasn’t one been written yet? Why hasn’t anybody reimplemented TypeScript in, say, Go or Rust?

TypeScript is a huge project under active development from a funded team of incredibly dedicated, experienced Microsoft employees — as well as an active community of power users and contributors. The TypeScript team receives the equivalent of millions of dollars a year in funding from employee compensation alone. A new version of TypeScript that adds type checking bugfixes and features releases every three months.

Can you imagine the Herculean difficulty for any team trying to keep up with TypeScript?

I hope for a day when there is a tool that can fully compete with TypeScript. Competition is good for an ecosystem. But it’s going to be years until a tool like that can develop.

Subset Reimplementation

TypeScript consists of several big areas of functionality, of which type checking is only one. The areas of type checking used by typed linting theoretically revolves around two areas of APIs:

Most typed lint rule in practice today only use the retrieval part of the type checker. Reimplementing only that portion of TypeScript could significantly reduce the development cost of the reimplementation.

Unfortunately, the relations portion of TypeScript was recently confirmed to be ready for use by linters7. typescript-eslint is going to start building in rule logic that builds on checker.isTypeAssignableTo.

Any subset reimplementation of TypeScript would need to include equivalents both for the existing type retrieval and type relations APIs. Those APIs include most of the tricky assignability logic that makes TypeScript so hard to reimplement in the first place.

A subset reimplementation of TypeScript for typed linting is tempting, but not as narrowly scoped of a task as it might seem. The scope of TypeScript that must be implemented is actually a substantial portion of its APIs’ logic.

Automatically Porting TypeScript to Native Speed

I previously touched on similar points in Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now > Option: Boosting TypeScript’s APIs to Native Speed.

Instead of reimplementing TypeScript from scratch, what if we could automatically port its source code to a faster language?

Well, TypeScript has tens of thousands of lines of source code built for single-threaded JavaScript with dynamic objects. It’s really not architected to work in another paradigm.

I think this approach is very promising for getting a TypeScript equivalent that stays up-to-date, but the difficulty is very high. I’ve yet to see anybody make any significant progress on this idea beyond proof-of-concepts.

Closing Thoughts

TypeScript itself is the only stable, production-ready tool available right now for typed linting on TypeScript code. It’s got a monopoly on usable type checking APIs. Being able to use a faster equivalent would be fantastic for users of typed linting, but none have become production ready yet.

There are a lot of obstacles to reimplementing TypeScript or an equivalent to it. And every year, TypeScript gets more complex and fully-featured.

I’m hopeful that a true competitor to TypeScript will eventually rise up and introduce more healthy competition into the ecosystem. Or failing that, either a subset of TypeScript that can speed up API consumers such as typed linting, or an automated TypeScript-to-native-speed port.

But, it’s going to be a while until anything remotely capable of competing with TypeScript stabilizes. We’re stuck with TypeScript for typed linting for now.

Footnotes

  1. facebook/react#25065 Bug: Eslint hooks returned by factory functions not linted

  2. vitest-dev/eslint-plugin-vitest#251 valid-type: use type checking to determine test name type?

  3. typescript-eslint.io > Rules > Extension & Type Information.

  4. dudykr/stc#1101 Project is officially abandoned

  5. marcj/TypeRunner Is there still a chance of kickstarting the project?

  6. Clarity on Flow’s Direction and Open Source Engagement

  7. 🔓 Intent to use: checker.isTypeAssignableTo


Liked this post? Thanks! Let the world know: