Utilities for working with TypeScript + ESLint together
—
The ESLintUtils namespace provides essential utilities for creating and configuring ESLint rules with TypeScript support.
import { ESLintUtils } from '@typescript-eslint/utils';// 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;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;
}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)}?.()`)
}]
});
}
}
};
}
});// 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;
}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
}
}// 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 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
});
}// 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
}// 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);
}// 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);
}
};
}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);
}
}
};
}
});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
}
};
}
});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