Search code examples
typescripteslintabstract-syntax-treetypescript-eslint

eslint no-restricted-syntax selector for preventing division of typescript branded type


I have the following typescript type: export type Milliseconds = number & { __type: 'milliseconds' };

and i want to prevent anyone using the division operator on it like so:

const foo = 1 as Milliseconds;
const bar = foo / 2;

i have come up with the following eslint rule

"no-restricted-syntax": [
    "error",
    {
        "selector": "BinaryExpression[left.typeAnnotation.typeName.name='Milliseconds'][operator='/']",
        "message": "Milliseconds cannot be divided directly, please use the msDivide()."
    },
],

but the rule only works when casting to milliseconds right before dividing so that this shows the error:

const foo = 1;
const bar = foo as Milliseconds / 2;

but this doesn't:

const foo = 1 as Milliseconds;
const bar = foo / 2;

from playing around with: https://typescript-eslint.io/play/#ts=5.0.4&sourceType=module&showAST=es it seems that the problem is that the AST doesnt keep a representation of the typeAnnotation in the identifier for foo

how do i write a selector that takes left and find's it's type based on it's name?


Solution

  • Selectors operate solely on the AST. In particular with the no-restricted-syntax rule you can only have one single selector that matches one node and reports on it - it's an intentionally very simple solution.

    You'll need to write a custom rule to do this check. For the specific case you've listed in your question - you could achieve this using scope analysis, however for the more general case you will need to make this a type-aware rule because scope analysis cannot act across function or module boundaries.

    For example consider this code:

    // fileA.ts
    declare function getCurrentTimeMillis(): Milliseconds;
    
    // fileB.ts
    import { getCurrentTimeMillis } from './fileA';
    
    const x = getCurrentTimeMillis();
    const y = x / 2; // ❌ OOPS
    

    This sort of check requires type information - you need to inspect the type of x to determine that it is of type Milliseconds.

    So the general logic of your custom rule would be

    1. match binary expressions that use the / operator.
    2. get the type of the left-hand expression.
    3. if the type is Milliseconds, then report.

    You can read more about typescript-eslint rules and type-aware rules here: https://typescript-eslint.io/custom-rules