Multiple rules from TypeScript ESLint requires type information. Moreover, several linter rules could be enhanced by using type information.
TypeScript ESLint uses the TypeScript Compiler API to get types. This architecture has the advantage of using the TypeScript Compiler. However, it has several drawbacks:
- TSC is slow, and using the Compiler API makes the slowness even more noticeable. In fact, it is so slow that TypeScript ESLint provides presets with and without the rules that require type information.
- Biome and TSC use their own AST, which makes interoperability difficult.
This is why we think it is important to implement our own type synthesiser. If we have a fast type synthesiser, then we could enhance many lint rules with a marginal performance overhead.
Note that we are not trying to implement a full-fledged type system or a type checker like TypeScript. Many attempt, even the most promising failed.
We believe that the Biome type synthesiser doesn't need to be perfect or handle complex TypeScript types. Even a rudimentary type synthesiser could be valuable to many lint rules.
TSC synthesizes types with a bottom-up approach. It first synthesizes the leaves of an expression and build on this knowledge to synthesize the types of the expression.
Type synthesis may require the evaluation of constant expressions.
For example, the type of 1 + 1
is 2
.
Synthesizing a type may also require a type environment that retains known information about the symbols.
For instance if the type environment knows that s
is a string,
then the type of the expression s + 0
is a string.
Referenced symbols can be imports. In this case, we need to access to the exported symbol of the imported file to retrieve its type.
Synthesized type should be distinguished values from the AST. However, we should have a way of requesting the node of a given type if it makes sense. For instance a class type can be associated to a class declaration.
To start we could use a type inference that infers the type of variable based on the expression that is assigned to it. An exception could be made for callbacks, where the type of the callback is inferred according to where it is passed.
In the following example the type of x
is inferred as string
.
declare let y: string;
type x = y;
In the following example the type of the callback is inferred as (a: string) => string
.
declare function map(arr: string[], f: (a: string) => string): string[];
map([""], (s) => s + " ");
The adoption of TypeScript and Flow by big projects highlighted the need of more performant type checkers and tools that rely on them.
To increase their performance, type checkers and tools adopted a new architecture in the past months or even years in the case of Flow.
Flow adopted the Type First architecture. TypeScript 5.5 introduces an Isolated Declaration mode. Tools such as documentation generators for JSR requires avoiding Slow Types.
All these approaches are roughly equivalent. They reduce the work required to synthesize the type of expressions by requiring explicit type annotations on the exported items of a module. Thus, synthesizing an expression that relies on an imported item doesn't require synthesizing the type of the imported item.
We could even go beyond and also consider any control flow root (class, function, ...) as a type boundary. Basically we could assume explicit types for function, class, method declarations, but callbacks.
Note that our type system should not throw an error if a type boundary is not explicit. In this case we could say that we don't know the type of the item. The linter should still be able to operate with partial type info.
In the following example, the return type of f
is not known.
Thus, the type of x
is unknown.
declare function f();
let x = f();
We could first write a visitor that synthesizes the type of exported items of a module (file). This will mainly require to design data structures to represent the types. Type synthesis should be very simple because we rely on explicit type for exported items, but for constant expressions that will require some work. I am unsure at the moment if we need the semantic model for type synthesis of exported items. This will require some investigation.
Another visitor could synthesize all items of a file. To do that, we will need to resolve imports and extract the type synthesis of the first visitor for the imported modules. Import resolution could also allow implementing other rules such as the rules of the ESLint Import plugin.
Note that we could avoid synthesizing all items of a file, by just synthesizing the required items. For instance, when you synthesize the types of local variables in a function, you don't need to synthesize the types of local variables in other functions. Once synthesized, the result could be cached.
The first subsection presents the rule we would like to implement in a first version of our type synthesiser. The second subsection presents rules that we are interested in implementing in the future.
-
useAwaitThenable
Source: https://typescript-eslint.io/rules/await-thenable/
Ensure that only thenable values are awaited. We could first target a rule that ensures that an awaited expression is a Promise. We could ignore values with an unknown type.
-
noFloatingPromises
Source: https://typescript-eslint.io/rules/no-floating-promises/
Ensure that a promise is handled (returned, awaited, ...).
-
noForInArray
Source: @typescript-eslint/no-for-in-array
Ensure that
for-in
is not used on arrays. -
noDuplicateLiteralEnumMembers
Source; https://typescript-eslint.io/rules/no-duplicate-enum-values
Ensure that every enum member initialized with a literal expression is unique. This doesn't necessarlly requires a type system. We need to compute literal expressions.
Note: this could be included in
useLiteralEnumMembers
?
-
noUselessCondition
Source: https://typescript-eslint.io/rules/no-unnecessary-condition/
A condition is useless if it is always truthy or falsy.
-
noObjectToString
Source: @typescript-eslint/no-base-to-string
Ensure that an object that is stringified has a
toString
implementation. -
noUselessTypeConstituents
SOurce: https://typescript-eslint.io/rules/no-redundant-type-constituents/ Source: https://typescript-eslint.io/rules/no-duplicate-type-constituents/
Ensure that a union or an intersection doesn't have useless constituents
-
useThrowError
Source: https://typescript-eslint.io/rules/only-throw-error/ Source: https://typescript-eslint.io/rules/no-throw-literal/
Ensure that only
Error
(or subclass) instances are thrown. -
useArrayFind
-
useArrayIncludes
-
useRetsrictedPlusOperands
Source: https://typescript-eslint.io/rules/restrict-plus-operands/
Ensure that operands of an addition have the same type.
-
noUnboundMethod
Source: https://typescript-eslint.io/rules/unbound-method/
Enforce unbound methods are called with their expected scope.
-
noUselessConstructor
This rule could be enhanced to solve biome#987.
-
useLiteralKeys
This rule could be enhanced by enabling computed keys for index signatures.
declare props: { [p: string]: unknown } props["a"]
-
noFallthroughSwitchClause
This rule could be enhanced by recognizing functions that return
never
. -
noConfusingExtends
Ensure that a class or an interface always
extends
a class or an interface.The check could also ban extending a computed value. Thus, implementing this rule doesn't require a type system. We need a way to resolve imports.
-
useNominalClass
Ensure that only instance of the class or of a subclass are passed for a variable typed with the class.
class A {} class B extends A {} class C {} declare let a: A; // VALID a = new A() a = new B() // subclass // INVALID a = new C() a = {}
-
useExactObjectShape
Ensure that literal objects which are assigned to a variable using an object type have exactly the same properties and in the same order.
This ensures monomorphic access (performance).
type Person = { name: string, age: number, } declare let p: Person // VALID p = { name: "Luke", age: 21 } // INVALID p = { age: 21, name: "Luke"} // wrong property order p = { name: "Luke"} // missing property p = { name: "Luke", age: 21, extra: null } // extra property