CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-typescript-eslint--utils

Utilities for working with TypeScript + ESLint together

Pending
Overview
Eval results
Files

eslint-utils.mddocs/

ESLint Utilities

The ESLintUtils namespace provides essential utilities for creating and configuring ESLint rules with TypeScript support.

Import

import { ESLintUtils } from '@typescript-eslint/utils';

Rule Creation

RuleCreator Function

// Create a rule creator with documentation URL generator
function RuleCreator<PluginDocs extends Record<string, unknown>>(
  urlCreator: (ruleName: string) => string
): <Options extends readonly unknown[], MessageIds extends string>(
  rule: RuleCreateAndOptions<Options, MessageIds> & { name: string }
) => RuleWithMetaAndName<Options, MessageIds, PluginDocs>;

// Create rule without documentation URLs
RuleCreator.withoutDocs: <Options extends readonly unknown[], MessageIds extends string>(
  args: RuleWithMeta<Options, MessageIds, NamedCreateRuleMetaDocs>
) => TSESLint.RuleModule<MessageIds, Options>;

// Usage examples
const createRule = ESLintUtils.RuleCreator(
  name => `https://typescript-eslint.io/rules/${name}`
);

const createRuleWithoutDocs = ESLintUtils.RuleCreator.withoutDocs;

Rule Definition Interface

interface RuleCreateAndOptions<Options extends readonly unknown[], MessageIds extends string> {
  name: string;
  meta: NamedCreateRuleMeta<MessageIds, PluginDocs, Options>;
  defaultOptions: Options;
  create: (
    context: TSESLint.RuleContext<MessageIds, Options>,
    optionsWithDefault: Options
  ) => TSESLint.RuleListener;
}

// Rule metadata interface
interface NamedCreateRuleMeta<
  MessageIds extends string,
  PluginDocs extends Record<string, unknown>,
  Options extends readonly unknown[]
> extends Omit<TSESLint.RuleMetaData<MessageIds, PluginDocs, Options>, 'docs'> {
  docs: NamedCreateRuleMetaDocs;
}

// Documentation interface  
interface NamedCreateRuleMetaDocs {
  description: string;
  recommended?: 'strict' | boolean;
  requiresTypeChecking?: boolean;
  extendsBaseRule?: boolean | string;
}

Complete Rule Example

import { ESLintUtils, TSESLint } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(
  name => `https://example.com/rules/${name}`
);

type Options = [{
  allowNumericLiterals?: boolean;
  allowBooleanLiterals?: boolean;
  allowNullishCoalescing?: boolean;
}];

type MessageIds = 'noUnsafeReturn' | 'noUnsafeAssignment' | 'suggestOptional';

export default createRule<Options, MessageIds>({
  name: 'no-unsafe-operations',
  meta: {
    type: 'problem',
    docs: {
      description: 'Disallow unsafe operations on potentially undefined values',
      recommended: 'strict',
      requiresTypeChecking: true
    },
    messages: {
      noUnsafeReturn: 'Unsafe return of potentially {{type}} value',
      noUnsafeAssignment: 'Unsafe assignment to {{target}}',
      suggestOptional: 'Consider using optional chaining: {{suggestion}}'
    },
    schema: [{
      type: 'object',
      properties: {
        allowNumericLiterals: { type: 'boolean' },
        allowBooleanLiterals: { type: 'boolean' },
        allowNullishCoalescing: { type: 'boolean' }
      },
      additionalProperties: false
    }],
    fixable: 'code',
    hasSuggestions: true
  },
  defaultOptions: [{
    allowNumericLiterals: false,
    allowBooleanLiterals: false,
    allowNullishCoalescing: true
  }],
  create(context, [options]) {
    // Rule implementation with typed context and options
    const services = ESLintUtils.getParserServices(context);
    const checker = services.program.getTypeChecker();
    
    return {
      CallExpression(node) {
        // Type-aware rule logic
        const tsNode = services.esTreeNodeToTSNodeMap.get(node);
        const type = checker.getTypeAtLocation(tsNode);
        
        if (type.flags & TypeScript.TypeFlags.Undefined) {
          context.report({
            node,
            messageId: 'noUnsafeReturn',
            data: { type: 'undefined' },
            suggest: [{
              messageId: 'suggestOptional',
              data: { suggestion: 'obj?.method()' },
              fix: (fixer) => fixer.replaceText(node, `${context.getSourceCode().getText(node.callee)}?.()`)
            }]
          });
        }
      }
    };
  }
});

Parser Services

Type-Aware Parsing

// Get parser services (overloaded)
function getParserServices(
  context: TSESLint.RuleContext<string, readonly unknown[]>
): ParserServices;

function getParserServices(
  context: TSESLint.RuleContext<string, readonly unknown[]>,
  allowWithoutFullTypeInformation: false
): ParserServicesWithTypeInformation;

function getParserServices(
  context: TSESLint.RuleContext<string, readonly unknown[]>,
  allowWithoutFullTypeInformation: true
): ParserServices;

// Parser services interfaces
interface ParserServices {
  program: TypeScript.Program | null;
  esTreeNodeToTSNodeMap: WeakMap<TSESTree.Node, TypeScript.Node>;
  tsNodeToESTreeNodeMap: WeakMap<TypeScript.Node, TSESTree.Node>;
  hasFullTypeInformation: boolean;
}

interface ParserServicesWithTypeInformation extends ParserServices {
  program: TypeScript.Program;
  hasFullTypeInformation: true;
}

Using Parser Services

import { ESLintUtils } from '@typescript-eslint/utils';

// In a rule that requires type information
create(context) {
  // Get services with type information required
  const services = ESLintUtils.getParserServices(context, false);
  const program = services.program; // TypeScript.Program (not null)
  const checker = program.getTypeChecker();
  
  return {
    Identifier(node) {
      // Convert ESTree node to TypeScript node
      const tsNode = services.esTreeNodeToTSNodeMap.get(node);
      
      // Get type information
      const type = checker.getTypeAtLocation(tsNode);
      const typeString = checker.typeToString(type);
      
      // Convert back to ESTree node if needed
      const esNode = services.tsNodeToESTreeNodeMap.get(tsNode);
    }
  };
}

// In a rule that works with or without type information  
create(context) {
  const services = ESLintUtils.getParserServices(context, true);
  
  if (services.hasFullTypeInformation) {
    // Type-aware logic
    const checker = services.program!.getTypeChecker();
  } else {
    // Syntax-only logic
    // services.program is null
  }
}

Option Handling

Default Options Application

// Apply default options to user options
function applyDefault<
  User extends readonly unknown[],
  Default extends readonly unknown[]
>(
  defaultOptions: Default,
  userOptions: User | null | undefined
): User extends readonly unknown[] 
  ? Default extends readonly [unknown, ...unknown[]]
    ? User extends readonly [unknown, ...unknown[]] 
      ? { [K in keyof Default]: K extends keyof User ? User[K] : Default[K] }
      : Default
    : User
  : Default;

// Usage examples
const defaultOptions = [{ strict: true, level: 'error' }] as const;
const userOptions = [{ strict: false }] as const;

const mergedOptions = ESLintUtils.applyDefault(defaultOptions, userOptions);
// Result: [{ strict: false, level: 'error' }]

// In rule definition
export default createRule({
  name: 'my-rule',
  defaultOptions: [{ 
    checkArrays: true,
    ignorePatterns: []
  }],
  create(context, optionsWithDefaults) {
    // optionsWithDefaults is fully typed with defaults applied
    const [{ checkArrays, ignorePatterns }] = optionsWithDefaults;
  }
});

Deep Object Merging

// Deep merge two objects
function deepMerge(first?: Record<string, unknown>, second?: Record<string, unknown>): Record<string, unknown>;

// Object type predicate
function isObjectNotArray(obj: unknown): obj is ObjectLike;

// Object-like type
type ObjectLike<T = unknown> = Record<string, T>;

// Usage examples
const merged = ESLintUtils.deepMerge(
  { 
    rules: { indent: 'error' },
    settings: { react: { version: '18' } }
  },
  {
    rules: { quotes: 'single' },
    settings: { react: { pragma: 'React' } }
  }
);
// Result: {
//   rules: { indent: 'error', quotes: 'single' },
//   settings: { react: { version: '18', pragma: 'React' } }
// }

if (ESLintUtils.isObjectNotArray(value)) {
  // value is Record<string, unknown>
  Object.keys(value).forEach(key => {
    // Safe object iteration
  });
}

Type Inference Utilities

Rule Type Extraction

// Infer Options type from RuleModule
type InferOptionsTypeFromRule<T> = T extends TSESLint.RuleModule<string, infer Options> ? Options : unknown;

// Infer MessageIds type from RuleModule  
type InferMessageIdsTypeFromRule<T> = T extends TSESLint.RuleModule<infer MessageIds, readonly unknown[]> ? MessageIds : unknown;

// Usage examples
declare const myRule: TSESLint.RuleModule<'error' | 'warning', [{ strict: boolean }]>;

type MyRuleOptions = ESLintUtils.InferOptionsTypeFromRule<typeof myRule>;
// Type: [{ strict: boolean }]

type MyRuleMessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof myRule>;  
// Type: 'error' | 'warning'

// Use in rule testing
function testRule<TRule extends TSESLint.RuleModule<string, readonly unknown[]>>(
  rule: TRule,
  tests: {
    valid: { 
      code: string;
      options?: ESLintUtils.InferOptionsTypeFromRule<TRule>;
    }[];
    invalid: {
      code: string;
      errors: { messageId: ESLintUtils.InferMessageIdsTypeFromRule<TRule> }[];
      options?: ESLintUtils.InferOptionsTypeFromRule<TRule>;
    }[];
  }
) {
  // Type-safe rule testing
}

Parser Detection

TypeScript-ESLint Parser Detection

// Check if parser appears to be @typescript-eslint/parser
function parserSeemsToBeTSESLint(parser: string | undefined): boolean;

// Usage examples
create(context) {
  const parserOptions = context.parserOptions;
  
  if (!ESLintUtils.parserSeemsToBeTSESLint(parserOptions.parser)) {
    // Rule may not work properly with non-TypeScript parser
    return {};
  }
  
  // Safe to use TypeScript-specific features
  const services = ESLintUtils.getParserServices(context);
}

Null Safety Utilities

Null Throws Function

// Assert value is not null/undefined with custom message
function nullThrows<T>(value: T | null | undefined, message: string): NonNullable<T>;

// Common null assertion reasons
const NullThrowsReasons = {
  MissingParent: 'Expected node to have a parent.',
  MissingToken: (token: string, thing: string) => `Expected to find a ${token} for the ${thing}.`
};

// Usage examples
create(context) {
  return {
    CallExpression(node) {
      // Assert parent exists
      const parent = ESLintUtils.nullThrows(
        node.parent,
        ESLintUtils.NullThrowsReasons.MissingParent
      );
      
      // Assert token exists
      const sourceCode = context.getSourceCode();
      const openParen = ESLintUtils.nullThrows(
        sourceCode.getTokenAfter(node.callee),
        ESLintUtils.NullThrowsReasons.MissingToken('(', 'call expression')
      );
      
      // Now parent and openParen are guaranteed non-null
      console.log(parent.type, openParen.value);
    }
  };
}

Advanced Rule Patterns

Type-Aware Rule with Services

import { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(name => `https://example.com/${name}`);

type Options = [{
  ignoreFunctionExpressions?: boolean;
  ignoreArrowFunctions?: boolean;
  ignoreMethodDefinitions?: boolean;
}];

type MessageIds = 'missingReturnType' | 'addReturnType';

export default createRule<Options, MessageIds>({
  name: 'explicit-function-return-type',
  meta: {
    type: 'problem',
    docs: {
      description: 'Require explicit return types on functions',
      requiresTypeChecking: true
    },
    messages: {
      missingReturnType: 'Function is missing return type annotation',
      addReturnType: 'Add explicit return type annotation'
    },
    schema: [{
      type: 'object',
      properties: {
        ignoreFunctionExpressions: { type: 'boolean' },
        ignoreArrowFunctions: { type: 'boolean' },
        ignoreMethodDefinitions: { type: 'boolean' }
      },
      additionalProperties: false
    }],
    fixable: 'code',
    hasSuggestions: true
  },
  defaultOptions: [{
    ignoreFunctionExpressions: false,
    ignoreArrowFunctions: false,
    ignoreMethodDefinitions: false
  }],
  create(context, [options]) {
    const services = ESLintUtils.getParserServices(context);
    const checker = services.program.getTypeChecker();
    const sourceCode = context.getSourceCode();
    
    function checkFunction(node: TSESTree.Function): void {
      // Skip if return type annotation exists
      if (node.returnType) return;
      
      // Apply option filters
      if (options.ignoreFunctionExpressions && node.type === 'FunctionExpression') return;
      if (options.ignoreArrowFunctions && node.type === 'ArrowFunctionExpression') return;
      
      // Get TypeScript type information
      const tsNode = services.esTreeNodeToTSNodeMap.get(node);
      const signature = checker.getSignatureFromDeclaration(tsNode as TypeScript.SignatureDeclaration);
      
      if (signature) {
        const returnType = checker.getReturnTypeOfSignature(signature);
        const returnTypeString = checker.typeToString(returnType);
        
        context.report({
          node: node.returnType ?? node,
          messageId: 'missingReturnType',
          suggest: [{
            messageId: 'addReturnType',
            fix: (fixer) => {
              const colon = node.params.length > 0 
                ? sourceCode.getTokenAfter(ESLintUtils.nullThrows(
                    sourceCode.getLastToken(node.params[node.params.length - 1]),
                    'Expected closing paren'
                  ))
                : sourceCode.getTokenAfter(node);
                
              const closeParen = ESLintUtils.nullThrows(colon, 'Expected closing paren');
              return fixer.insertTextAfter(closeParen, `: ${returnTypeString}`);
            }
          }]
        });
      }
    }
    
    return {
      FunctionDeclaration: checkFunction,
      FunctionExpression: checkFunction,
      ArrowFunctionExpression: checkFunction,
      MethodDefinition(node) {
        if (!options.ignoreMethodDefinitions && node.value.type === 'FunctionExpression') {
          checkFunction(node.value);
        }
      }
    };
  }
});

Rule with Complex Options Handling

type ComplexOptions = [
  {
    mode: 'strict' | 'loose';
    overrides?: {
      [pattern: string]: {
        mode?: 'strict' | 'loose';
        ignore?: boolean;
      };
    };
    globalIgnorePatterns?: string[];
  }
];

export default createRule<ComplexOptions, 'violation'>({
  name: 'complex-rule',
  meta: {
    type: 'problem',
    docs: { description: 'Complex rule with nested options' },
    messages: {
      violation: 'Rule violation in {{mode}} mode'
    },
    schema: [{
      type: 'object',
      properties: {
        mode: { enum: ['strict', 'loose'] },
        overrides: {
          type: 'object',
          patternProperties: {
            '.*': {
              type: 'object',
              properties: {
                mode: { enum: ['strict', 'loose'] },
                ignore: { type: 'boolean' }
              },
              additionalProperties: false
            }
          }
        },
        globalIgnorePatterns: {
          type: 'array',
          items: { type: 'string' }
        }
      },
      additionalProperties: false,
      required: ['mode']
    }]
  },
  defaultOptions: [{
    mode: 'strict',
    overrides: {},
    globalIgnorePatterns: []
  }],
  create(context, [options]) {
    // Access deeply merged options with full type safety
    const { mode, overrides, globalIgnorePatterns } = options;
    
    function getEffectiveOptions(fileName: string) {
      // Check overrides
      for (const [pattern, override] of Object.entries(overrides ?? {})) {
        if (new RegExp(pattern).test(fileName)) {
          return {
            mode: override.mode ?? mode,
            ignore: override.ignore ?? false
          };
        }
      }
      
      return { mode, ignore: false };
    }
    
    return {
      Program() {
        const fileName = context.getFilename();
        const effectiveOptions = getEffectiveOptions(fileName);
        
        if (effectiveOptions.ignore) return;
        
        // Rule logic based on effective options
      }
    };
  }
});

Utility Composition Example

import { ESLintUtils, ASTUtils, TSESLint } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(name => `https://example.com/${name}`);

export default createRule({
  name: 'comprehensive-example',
  meta: {
    type: 'suggestion',
    docs: { description: 'Demonstrates ESLintUtils composition' },
    messages: {
      issue: 'Issue detected: {{description}}'
    },
    schema: []
  },
  defaultOptions: [],
  create(context) {
    // Combine multiple utilities
    const services = ESLintUtils.getParserServices(context, true);
    const sourceCode = context.getSourceCode();
    
    return {
      CallExpression(node) {
        // Use parser services if available
        if (services.hasFullTypeInformation) {
          const tsNode = services.esTreeNodeToTSNodeMap.get(node);
          const type = services.program!.getTypeChecker().getTypeAtLocation(tsNode);
        }
        
        // Use AST utilities
        if (ASTUtils.isOptionalCallExpression(node)) {
          context.report({
            node,
            messageId: 'issue',
            data: { description: 'optional call detected' }
          });
        }
        
        // Use null safety
        const parent = ESLintUtils.nullThrows(
          node.parent,
          ESLintUtils.NullThrowsReasons.MissingParent
        );
      }
    };
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-typescript-eslint--utils

docs

ast-utils.md

eslint-utils.md

index.md

json-schema.md

ts-eslint.md

ts-utils.md

tile.json