(Experimental) Utilities for working with TypeScript + ESLint together
—
Rule creation, testing, and configuration utilities specifically designed for TypeScript ESLint rules. These utilities provide a complete toolkit for developing, testing, and configuring ESLint rules with full TypeScript integration.
Utilities for creating TypeScript-aware ESLint rules with automatic documentation and type safety.
/**
* Creates a rule creator function with automatic documentation URL generation
* @param urlCreator - Function that generates documentation URLs from rule names
* @returns Function for creating rules with automatic docs URLs
*/
function RuleCreator(urlCreator: (ruleName: string) => string): <
TOptions extends readonly unknown[],
TMessageIds extends string,
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener
>(ruleDefinition: Readonly<TSESLint.RuleModule<TMessageIds, TOptions, TRuleListener>>) => TSESLint.RuleModule<TMessageIds, TOptions>;
/**
* Creates a rule without automatic documentation URL
* @param ruleDefinition - Rule definition object
* @returns ESLint rule module
*/
declare namespace RuleCreator {
function withoutDocs<
TOptions extends readonly unknown[],
TMessageIds extends string,
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener
>(ruleDefinition: Readonly<TSESLint.RuleModule<TMessageIds, TOptions, TRuleListener>>): TSESLint.RuleModule<TMessageIds, TOptions>;
}Usage Example:
import { ESLintUtils, TSESLint } from "@typescript-eslint/experimental-utils";
const createRule = ESLintUtils.RuleCreator(
name => `https://typescript-eslint.io/rules/${name}`
);
const rule = createRule({
name: 'my-rule',
meta: {
type: 'problem',
messages: {
error: 'This is an error message'
},
schema: []
},
defaultOptions: [],
create(context: TSESLint.RuleContext<'error', []>) {
return {
FunctionDeclaration(node) {
context.report({
node,
messageId: 'error'
});
}
};
}
});Utilities for accessing TypeScript compiler services within ESLint rules.
/**
* Retrieves TypeScript parser services from the rule context
* @param context - ESLint rule context
* @param allowWithoutFullTypeInformation - Whether to allow partial type information
* @returns Parser services object with TypeScript compiler access
* @throws Error if parser services are not available
*/
function getParserServices<TMessageIds extends string, TOptions extends readonly unknown[]>(
context: TSESLint.RuleContext<TMessageIds, TOptions>,
allowWithoutFullTypeInformation?: boolean
): TSESTree.ParserServices;Usage Example:
import { ESLintUtils, TSESTree } from "@typescript-eslint/experimental-utils";
const rule = ESLintUtils.RuleCreator(url => url)({
name: 'type-aware-rule',
meta: {
type: 'problem',
messages: { error: 'Type error' },
schema: []
},
defaultOptions: [],
create(context) {
const services = ESLintUtils.getParserServices(context);
const checker = services.program?.getTypeChecker();
return {
Identifier(node) {
if (checker) {
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(tsNode);
// Use TypeScript type information
}
}
};
}
});Utilities for handling rule options and configuration merging.
/**
* Applies default options to user-provided options using deep merge
* @param defaultOptions - Default option values
* @param userOptions - User-provided options (can be null)
* @returns Merged options with defaults applied
*/
function applyDefault<TUser, TDefault>(
defaultOptions: Readonly<TDefault>,
userOptions: Readonly<TUser> | null
): TDefault;
/**
* Deep merges two objects, combining properties recursively
* @param first - First object to merge
* @param second - Second object to merge
* @returns Merged object with combined properties
*/
function deepMerge(first?: ObjectLike, second?: ObjectLike): Record<string, unknown>;
/**
* Type guard to check if a value is an object but not an array
* @param obj - Value to check
* @returns True if the value is an object (not array)
*/
function isObjectNotArray<T = Record<string, unknown>>(obj: unknown): obj is T;
interface ObjectLike {
[key: string]: unknown;
}Usage Example:
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
interface MyRuleOptions {
checkTypes: boolean;
ignorePatterns: string[];
severity: 'error' | 'warn';
}
const defaultOptions: MyRuleOptions = {
checkTypes: true,
ignorePatterns: [],
severity: 'error'
};
const rule = ESLintUtils.RuleCreator(url => url)({
name: 'my-rule',
meta: {
type: 'problem',
messages: { error: 'Error' },
schema: [/* schema */]
},
defaultOptions: [defaultOptions],
create(context, [userOptions]) {
const options = ESLintUtils.applyDefault(defaultOptions, userOptions);
// options now has all properties with defaults applied
return {
// rule implementation
};
}
});Type utilities for inferring types from rule definitions.
/**
* Infers the options type from a rule module
*/
type InferOptionsTypeFromRule<T> = T extends TSESLint.RuleModule<string, infer TOptions, TSESLint.RuleListener> ? TOptions : unknown;
/**
* Infers the message IDs type from a rule module
*/
type InferMessageIdsTypeFromRule<T> = T extends TSESLint.RuleModule<infer TMessageIds, readonly unknown[], TSESLint.RuleListener> ? TMessageIds : string;Helper functions for common rule development tasks.
/**
* Non-null assertion with custom error message
* @param value - Value to check for null/undefined
* @param message - Error message if value is null/undefined
* @returns Non-null value
* @throws Error if value is null or undefined
*/
function nullThrows<T>(value: T | null | undefined, message: string): T;
/**
* Common error messages for nullThrows function
*/
const NullThrowsReasons: {
readonly MissingParent: "Expected node to have a parent.";
readonly MissingToken: "Expected to find a token.";
};Usage Example:
import { ESLintUtils, TSESTree } from "@typescript-eslint/experimental-utils";
function analyzeNode(node: TSESTree.Node): void {
const parent = ESLintUtils.nullThrows(
node.parent,
ESLintUtils.NullThrowsReasons.MissingParent
);
// parent is now guaranteed to be non-null
}Enhanced rule testing utilities with TypeScript support and batch test processing.
/**
* Enhanced ESLint rule tester with TypeScript support
*/
class RuleTester {
/**
* Creates a new rule tester with base configuration
* @param baseOptions - Base configuration for the rule tester
*/
constructor(baseOptions: TSESLint.RuleTesterConfig);
/**
* Runs tests for a specific rule with TypeScript support
* @param name - Name of the rule being tested
* @param rule - Rule module to test
* @param tests - Test cases (valid and invalid)
*/
run<TMessageIds extends string, TOptions extends readonly unknown[]>(
name: string,
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
tests: TSESLint.RunTests<TMessageIds, TOptions>
): void;
/**
* Static cleanup function called after all tests
*/
static afterAll: (() => void) | undefined;
}
/**
* Template tag to mark code as "no format" for testing purposes
* @param raw - Template strings array
* @param keys - Template substitution values
* @returns Formatted string marked as no-format
*/
function noFormat(raw: TemplateStringsArray, ...keys: string[]): string;
/**
* Converts batch test cases into individual test cases
* @param test - Batch test configuration
* @returns Array of individual test cases
*/
function batchedSingleLineTests<TOptions extends readonly unknown[]>(test: {
code: string;
options?: TOptions;
skip?: boolean;
only?: boolean;
}): TSESLint.ValidTestCase<TOptions>[];
function batchedSingleLineTests<TMessageIds extends string, TOptions extends readonly unknown[]>(test: {
code: string;
options?: TOptions;
skip?: boolean;
only?: boolean;
errors: TestCaseError<TMessageIds>[];
output?: string | null;
}): TSESLint.InvalidTestCase<TMessageIds, TOptions>[];
interface TestCaseError<TMessageIds extends string> {
messageId: TMessageIds;
data?: Record<string, unknown>;
type?: string;
line?: number;
column?: number;
endLine?: number;
endColumn?: number;
suggestions?: SuggestionOutput<TMessageIds>[];
}
interface SuggestionOutput<TMessageIds extends string> {
messageId: TMessageIds;
data?: Record<string, unknown>;
output: string;
desc?: string;
}Usage Example:
import { ESLintUtils, TSESLint } from "@typescript-eslint/experimental-utils";
const ruleTester = new ESLintUtils.RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: './tsconfig.json'
}
});
const rule = ESLintUtils.RuleCreator(url => url)({
name: 'test-rule',
meta: {
type: 'problem',
messages: {
error: 'Found error in {{name}}'
},
schema: []
},
defaultOptions: [],
create(context) {
return {
FunctionDeclaration(node) {
context.report({
node,
messageId: 'error',
data: { name: node.id?.name || 'unknown' }
});
}
};
}
});
ruleTester.run('test-rule', rule, {
valid: [
'const x = 1;',
'class MyClass {}',
{
code: 'const arrow = () => {};',
parserOptions: { ecmaVersion: 6 }
}
],
invalid: [
{
code: 'function foo() {}',
errors: [{
messageId: 'error',
data: { name: 'foo' },
line: 1,
column: 1
}]
},
{
code: ESLintUtils.noFormat`
function bar() {
// some code
}
`,
errors: [{ messageId: 'error' }]
}
]
});
// Using batched tests
const batchTests = ESLintUtils.batchedSingleLineTests({
code: `
function a() {}
function b() {}
function c() {}
`,
errors: [
{ messageId: 'error', line: 2 },
{ messageId: 'error', line: 3 },
{ messageId: 'error', line: 4 }
]
});Utilities for checking package version dependencies in tests.
/**
* Checks if all specified dependency version constraints are satisfied
* @param dependencyConstraints - Object mapping package names to version constraints
* @returns True if all constraints are satisfied
*/
function satisfiesAllDependencyConstraints(
dependencyConstraints?: DependencyConstraint
): boolean;
/**
* Interface for specifying package version constraints
*/
interface DependencyConstraint {
[packageName: string]: VersionConstraint;
}
type VersionConstraint = string | Range | SemVer;
interface Range {
range: string;
raw: string;
loose: boolean;
includePrerelease: boolean;
test(version: string | SemVer): boolean;
intersects(range: Range): boolean;
}
interface SemVer {
version: string;
major: number;
minor: number;
patch: number;
prerelease: readonly (string | number)[];
build: readonly string[];
compare(other: string | SemVer): -1 | 0 | 1;
compareMain(other: string | SemVer): -1 | 0 | 1;
}Usage Example:
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
// Check if TypeScript version meets requirements
const hasRequiredTypescript = ESLintUtils.satisfiesAllDependencyConstraints({
typescript: '>=4.0.0'
});
if (hasRequiredTypescript) {
// Run TypeScript-specific tests
} else {
// Skip tests that require newer TypeScript
}/**
* Valid test case configuration
*/
interface ValidTestCase<TOptions extends readonly unknown[]> {
/** Source code that should not trigger any errors */
code: string;
/** Rule options to use for this test */
options?: TOptions;
/** Filename for the test case */
filename?: string;
/** Parser options specific to this test */
parserOptions?: TSESLint.ParserOptions;
/** ESLint settings for this test */
settings?: Record<string, unknown>;
/** Parser to use (defaults to configured parser) */
parser?: string;
/** Global variables available in this test */
globals?: Record<string, boolean>;
/** Environment settings for this test */
env?: TSESLint.Linter.Environment;
/** Skip this test case */
skip?: boolean;
/** Run only this test case */
only?: boolean;
/** Dependency constraints for this test */
dependencyConstraints?: DependencyConstraint;
}
/**
* Invalid test case configuration
*/
interface InvalidTestCase<TMessageIds extends string, TOptions extends readonly unknown[]>
extends ValidTestCase<TOptions> {
/** Expected errors from this test case */
errors: TestCaseError<TMessageIds>[];
/** Expected output after fixes are applied */
output?: string | null;
}
/**
* Complete test suite for a rule
*/
interface RunTests<TMessageIds extends string, TOptions extends readonly unknown[]> {
/** Test cases that should not produce any errors */
valid: (string | ValidTestCase<TOptions>)[];
/** Test cases that should produce errors */
invalid: InvalidTestCase<TMessageIds, TOptions>[];
}/**
* Configuration for the ESLint rule tester
*/
interface RuleTesterConfig {
/** Parser to use for parsing test code */
parser?: string;
/** Parser options passed to the parser */
parserOptions?: TSESLint.ParserOptions;
/** Global variables available during testing */
globals?: Record<string, boolean>;
/** Environment settings */
env?: TSESLint.Linter.Environment;
/** ESLint settings object */
settings?: Record<string, unknown>;
/** Language options for the parser */
languageOptions?: {
ecmaVersion?: TSESLint.ParserOptions['ecmaVersion'];
sourceType?: TSESLint.ParserOptions['sourceType'];
globals?: Record<string, boolean>;
parser?: { parse(text: string, options?: any): any };
parserOptions?: Record<string, unknown>;
};
/** Linter options */
linterOptions?: {
noInlineConfig?: boolean;
reportUnusedDisableDirectives?: boolean;
};
}Install with Tessl CLI
npx tessl i tessl/npm-typescript-eslint--experimental-utils