0
# ESLint Utilities
1
2
The `ESLintUtils` namespace provides essential utilities for creating and configuring ESLint rules with TypeScript support.
3
4
## Import
5
6
```typescript { .api }
7
import { ESLintUtils } from '@typescript-eslint/utils';
8
```
9
10
## Rule Creation
11
12
### RuleCreator Function
13
14
```typescript { .api }
15
// Create a rule creator with documentation URL generator
16
function RuleCreator<PluginDocs extends Record<string, unknown>>(
17
urlCreator: (ruleName: string) => string
18
): <Options extends readonly unknown[], MessageIds extends string>(
19
rule: RuleCreateAndOptions<Options, MessageIds> & { name: string }
20
) => RuleWithMetaAndName<Options, MessageIds, PluginDocs>;
21
22
// Create rule without documentation URLs
23
RuleCreator.withoutDocs: <Options extends readonly unknown[], MessageIds extends string>(
24
args: RuleWithMeta<Options, MessageIds, NamedCreateRuleMetaDocs>
25
) => TSESLint.RuleModule<MessageIds, Options>;
26
27
// Usage examples
28
const createRule = ESLintUtils.RuleCreator(
29
name => `https://typescript-eslint.io/rules/${name}`
30
);
31
32
const createRuleWithoutDocs = ESLintUtils.RuleCreator.withoutDocs;
33
```
34
35
### Rule Definition Interface
36
37
```typescript { .api }
38
interface RuleCreateAndOptions<Options extends readonly unknown[], MessageIds extends string> {
39
name: string;
40
meta: NamedCreateRuleMeta<MessageIds, PluginDocs, Options>;
41
defaultOptions: Options;
42
create: (
43
context: TSESLint.RuleContext<MessageIds, Options>,
44
optionsWithDefault: Options
45
) => TSESLint.RuleListener;
46
}
47
48
// Rule metadata interface
49
interface NamedCreateRuleMeta<
50
MessageIds extends string,
51
PluginDocs extends Record<string, unknown>,
52
Options extends readonly unknown[]
53
> extends Omit<TSESLint.RuleMetaData<MessageIds, PluginDocs, Options>, 'docs'> {
54
docs: NamedCreateRuleMetaDocs;
55
}
56
57
// Documentation interface
58
interface NamedCreateRuleMetaDocs {
59
description: string;
60
recommended?: 'strict' | boolean;
61
requiresTypeChecking?: boolean;
62
extendsBaseRule?: boolean | string;
63
}
64
```
65
66
### Complete Rule Example
67
68
```typescript { .api }
69
import { ESLintUtils, TSESLint } from '@typescript-eslint/utils';
70
71
const createRule = ESLintUtils.RuleCreator(
72
name => `https://example.com/rules/${name}`
73
);
74
75
type Options = [{
76
allowNumericLiterals?: boolean;
77
allowBooleanLiterals?: boolean;
78
allowNullishCoalescing?: boolean;
79
}];
80
81
type MessageIds = 'noUnsafeReturn' | 'noUnsafeAssignment' | 'suggestOptional';
82
83
export default createRule<Options, MessageIds>({
84
name: 'no-unsafe-operations',
85
meta: {
86
type: 'problem',
87
docs: {
88
description: 'Disallow unsafe operations on potentially undefined values',
89
recommended: 'strict',
90
requiresTypeChecking: true
91
},
92
messages: {
93
noUnsafeReturn: 'Unsafe return of potentially {{type}} value',
94
noUnsafeAssignment: 'Unsafe assignment to {{target}}',
95
suggestOptional: 'Consider using optional chaining: {{suggestion}}'
96
},
97
schema: [{
98
type: 'object',
99
properties: {
100
allowNumericLiterals: { type: 'boolean' },
101
allowBooleanLiterals: { type: 'boolean' },
102
allowNullishCoalescing: { type: 'boolean' }
103
},
104
additionalProperties: false
105
}],
106
fixable: 'code',
107
hasSuggestions: true
108
},
109
defaultOptions: [{
110
allowNumericLiterals: false,
111
allowBooleanLiterals: false,
112
allowNullishCoalescing: true
113
}],
114
create(context, [options]) {
115
// Rule implementation with typed context and options
116
const services = ESLintUtils.getParserServices(context);
117
const checker = services.program.getTypeChecker();
118
119
return {
120
CallExpression(node) {
121
// Type-aware rule logic
122
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
123
const type = checker.getTypeAtLocation(tsNode);
124
125
if (type.flags & TypeScript.TypeFlags.Undefined) {
126
context.report({
127
node,
128
messageId: 'noUnsafeReturn',
129
data: { type: 'undefined' },
130
suggest: [{
131
messageId: 'suggestOptional',
132
data: { suggestion: 'obj?.method()' },
133
fix: (fixer) => fixer.replaceText(node, `${context.getSourceCode().getText(node.callee)}?.()`)
134
}]
135
});
136
}
137
}
138
};
139
}
140
});
141
```
142
143
## Parser Services
144
145
### Type-Aware Parsing
146
147
```typescript { .api }
148
// Get parser services (overloaded)
149
function getParserServices(
150
context: TSESLint.RuleContext<string, readonly unknown[]>
151
): ParserServices;
152
153
function getParserServices(
154
context: TSESLint.RuleContext<string, readonly unknown[]>,
155
allowWithoutFullTypeInformation: false
156
): ParserServicesWithTypeInformation;
157
158
function getParserServices(
159
context: TSESLint.RuleContext<string, readonly unknown[]>,
160
allowWithoutFullTypeInformation: true
161
): ParserServices;
162
163
// Parser services interfaces
164
interface ParserServices {
165
program: TypeScript.Program | null;
166
esTreeNodeToTSNodeMap: WeakMap<TSESTree.Node, TypeScript.Node>;
167
tsNodeToESTreeNodeMap: WeakMap<TypeScript.Node, TSESTree.Node>;
168
hasFullTypeInformation: boolean;
169
}
170
171
interface ParserServicesWithTypeInformation extends ParserServices {
172
program: TypeScript.Program;
173
hasFullTypeInformation: true;
174
}
175
```
176
177
### Using Parser Services
178
179
```typescript { .api }
180
import { ESLintUtils } from '@typescript-eslint/utils';
181
182
// In a rule that requires type information
183
create(context) {
184
// Get services with type information required
185
const services = ESLintUtils.getParserServices(context, false);
186
const program = services.program; // TypeScript.Program (not null)
187
const checker = program.getTypeChecker();
188
189
return {
190
Identifier(node) {
191
// Convert ESTree node to TypeScript node
192
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
193
194
// Get type information
195
const type = checker.getTypeAtLocation(tsNode);
196
const typeString = checker.typeToString(type);
197
198
// Convert back to ESTree node if needed
199
const esNode = services.tsNodeToESTreeNodeMap.get(tsNode);
200
}
201
};
202
}
203
204
// In a rule that works with or without type information
205
create(context) {
206
const services = ESLintUtils.getParserServices(context, true);
207
208
if (services.hasFullTypeInformation) {
209
// Type-aware logic
210
const checker = services.program!.getTypeChecker();
211
} else {
212
// Syntax-only logic
213
// services.program is null
214
}
215
}
216
```
217
218
## Option Handling
219
220
### Default Options Application
221
222
```typescript { .api }
223
// Apply default options to user options
224
function applyDefault<
225
User extends readonly unknown[],
226
Default extends readonly unknown[]
227
>(
228
defaultOptions: Default,
229
userOptions: User | null | undefined
230
): User extends readonly unknown[]
231
? Default extends readonly [unknown, ...unknown[]]
232
? User extends readonly [unknown, ...unknown[]]
233
? { [K in keyof Default]: K extends keyof User ? User[K] : Default[K] }
234
: Default
235
: User
236
: Default;
237
238
// Usage examples
239
const defaultOptions = [{ strict: true, level: 'error' }] as const;
240
const userOptions = [{ strict: false }] as const;
241
242
const mergedOptions = ESLintUtils.applyDefault(defaultOptions, userOptions);
243
// Result: [{ strict: false, level: 'error' }]
244
245
// In rule definition
246
export default createRule({
247
name: 'my-rule',
248
defaultOptions: [{
249
checkArrays: true,
250
ignorePatterns: []
251
}],
252
create(context, optionsWithDefaults) {
253
// optionsWithDefaults is fully typed with defaults applied
254
const [{ checkArrays, ignorePatterns }] = optionsWithDefaults;
255
}
256
});
257
```
258
259
### Deep Object Merging
260
261
```typescript { .api }
262
// Deep merge two objects
263
function deepMerge(first?: Record<string, unknown>, second?: Record<string, unknown>): Record<string, unknown>;
264
265
// Object type predicate
266
function isObjectNotArray(obj: unknown): obj is ObjectLike;
267
268
// Object-like type
269
type ObjectLike<T = unknown> = Record<string, T>;
270
271
// Usage examples
272
const merged = ESLintUtils.deepMerge(
273
{
274
rules: { indent: 'error' },
275
settings: { react: { version: '18' } }
276
},
277
{
278
rules: { quotes: 'single' },
279
settings: { react: { pragma: 'React' } }
280
}
281
);
282
// Result: {
283
// rules: { indent: 'error', quotes: 'single' },
284
// settings: { react: { version: '18', pragma: 'React' } }
285
// }
286
287
if (ESLintUtils.isObjectNotArray(value)) {
288
// value is Record<string, unknown>
289
Object.keys(value).forEach(key => {
290
// Safe object iteration
291
});
292
}
293
```
294
295
## Type Inference Utilities
296
297
### Rule Type Extraction
298
299
```typescript { .api }
300
// Infer Options type from RuleModule
301
type InferOptionsTypeFromRule<T> = T extends TSESLint.RuleModule<string, infer Options> ? Options : unknown;
302
303
// Infer MessageIds type from RuleModule
304
type InferMessageIdsTypeFromRule<T> = T extends TSESLint.RuleModule<infer MessageIds, readonly unknown[]> ? MessageIds : unknown;
305
306
// Usage examples
307
declare const myRule: TSESLint.RuleModule<'error' | 'warning', [{ strict: boolean }]>;
308
309
type MyRuleOptions = ESLintUtils.InferOptionsTypeFromRule<typeof myRule>;
310
// Type: [{ strict: boolean }]
311
312
type MyRuleMessageIds = ESLintUtils.InferMessageIdsTypeFromRule<typeof myRule>;
313
// Type: 'error' | 'warning'
314
315
// Use in rule testing
316
function testRule<TRule extends TSESLint.RuleModule<string, readonly unknown[]>>(
317
rule: TRule,
318
tests: {
319
valid: {
320
code: string;
321
options?: ESLintUtils.InferOptionsTypeFromRule<TRule>;
322
}[];
323
invalid: {
324
code: string;
325
errors: { messageId: ESLintUtils.InferMessageIdsTypeFromRule<TRule> }[];
326
options?: ESLintUtils.InferOptionsTypeFromRule<TRule>;
327
}[];
328
}
329
) {
330
// Type-safe rule testing
331
}
332
```
333
334
## Parser Detection
335
336
### TypeScript-ESLint Parser Detection
337
338
```typescript { .api }
339
// Check if parser appears to be @typescript-eslint/parser
340
function parserSeemsToBeTSESLint(parser: string | undefined): boolean;
341
342
// Usage examples
343
create(context) {
344
const parserOptions = context.parserOptions;
345
346
if (!ESLintUtils.parserSeemsToBeTSESLint(parserOptions.parser)) {
347
// Rule may not work properly with non-TypeScript parser
348
return {};
349
}
350
351
// Safe to use TypeScript-specific features
352
const services = ESLintUtils.getParserServices(context);
353
}
354
```
355
356
## Null Safety Utilities
357
358
### Null Throws Function
359
360
```typescript { .api }
361
// Assert value is not null/undefined with custom message
362
function nullThrows<T>(value: T | null | undefined, message: string): NonNullable<T>;
363
364
// Common null assertion reasons
365
const NullThrowsReasons = {
366
MissingParent: 'Expected node to have a parent.',
367
MissingToken: (token: string, thing: string) => `Expected to find a ${token} for the ${thing}.`
368
};
369
370
// Usage examples
371
create(context) {
372
return {
373
CallExpression(node) {
374
// Assert parent exists
375
const parent = ESLintUtils.nullThrows(
376
node.parent,
377
ESLintUtils.NullThrowsReasons.MissingParent
378
);
379
380
// Assert token exists
381
const sourceCode = context.getSourceCode();
382
const openParen = ESLintUtils.nullThrows(
383
sourceCode.getTokenAfter(node.callee),
384
ESLintUtils.NullThrowsReasons.MissingToken('(', 'call expression')
385
);
386
387
// Now parent and openParen are guaranteed non-null
388
console.log(parent.type, openParen.value);
389
}
390
};
391
}
392
```
393
394
## Advanced Rule Patterns
395
396
### Type-Aware Rule with Services
397
398
```typescript { .api }
399
import { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';
400
401
const createRule = ESLintUtils.RuleCreator(name => `https://example.com/${name}`);
402
403
type Options = [{
404
ignoreFunctionExpressions?: boolean;
405
ignoreArrowFunctions?: boolean;
406
ignoreMethodDefinitions?: boolean;
407
}];
408
409
type MessageIds = 'missingReturnType' | 'addReturnType';
410
411
export default createRule<Options, MessageIds>({
412
name: 'explicit-function-return-type',
413
meta: {
414
type: 'problem',
415
docs: {
416
description: 'Require explicit return types on functions',
417
requiresTypeChecking: true
418
},
419
messages: {
420
missingReturnType: 'Function is missing return type annotation',
421
addReturnType: 'Add explicit return type annotation'
422
},
423
schema: [{
424
type: 'object',
425
properties: {
426
ignoreFunctionExpressions: { type: 'boolean' },
427
ignoreArrowFunctions: { type: 'boolean' },
428
ignoreMethodDefinitions: { type: 'boolean' }
429
},
430
additionalProperties: false
431
}],
432
fixable: 'code',
433
hasSuggestions: true
434
},
435
defaultOptions: [{
436
ignoreFunctionExpressions: false,
437
ignoreArrowFunctions: false,
438
ignoreMethodDefinitions: false
439
}],
440
create(context, [options]) {
441
const services = ESLintUtils.getParserServices(context);
442
const checker = services.program.getTypeChecker();
443
const sourceCode = context.getSourceCode();
444
445
function checkFunction(node: TSESTree.Function): void {
446
// Skip if return type annotation exists
447
if (node.returnType) return;
448
449
// Apply option filters
450
if (options.ignoreFunctionExpressions && node.type === 'FunctionExpression') return;
451
if (options.ignoreArrowFunctions && node.type === 'ArrowFunctionExpression') return;
452
453
// Get TypeScript type information
454
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
455
const signature = checker.getSignatureFromDeclaration(tsNode as TypeScript.SignatureDeclaration);
456
457
if (signature) {
458
const returnType = checker.getReturnTypeOfSignature(signature);
459
const returnTypeString = checker.typeToString(returnType);
460
461
context.report({
462
node: node.returnType ?? node,
463
messageId: 'missingReturnType',
464
suggest: [{
465
messageId: 'addReturnType',
466
fix: (fixer) => {
467
const colon = node.params.length > 0
468
? sourceCode.getTokenAfter(ESLintUtils.nullThrows(
469
sourceCode.getLastToken(node.params[node.params.length - 1]),
470
'Expected closing paren'
471
))
472
: sourceCode.getTokenAfter(node);
473
474
const closeParen = ESLintUtils.nullThrows(colon, 'Expected closing paren');
475
return fixer.insertTextAfter(closeParen, `: ${returnTypeString}`);
476
}
477
}]
478
});
479
}
480
}
481
482
return {
483
FunctionDeclaration: checkFunction,
484
FunctionExpression: checkFunction,
485
ArrowFunctionExpression: checkFunction,
486
MethodDefinition(node) {
487
if (!options.ignoreMethodDefinitions && node.value.type === 'FunctionExpression') {
488
checkFunction(node.value);
489
}
490
}
491
};
492
}
493
});
494
```
495
496
### Rule with Complex Options Handling
497
498
```typescript { .api }
499
type ComplexOptions = [
500
{
501
mode: 'strict' | 'loose';
502
overrides?: {
503
[pattern: string]: {
504
mode?: 'strict' | 'loose';
505
ignore?: boolean;
506
};
507
};
508
globalIgnorePatterns?: string[];
509
}
510
];
511
512
export default createRule<ComplexOptions, 'violation'>({
513
name: 'complex-rule',
514
meta: {
515
type: 'problem',
516
docs: { description: 'Complex rule with nested options' },
517
messages: {
518
violation: 'Rule violation in {{mode}} mode'
519
},
520
schema: [{
521
type: 'object',
522
properties: {
523
mode: { enum: ['strict', 'loose'] },
524
overrides: {
525
type: 'object',
526
patternProperties: {
527
'.*': {
528
type: 'object',
529
properties: {
530
mode: { enum: ['strict', 'loose'] },
531
ignore: { type: 'boolean' }
532
},
533
additionalProperties: false
534
}
535
}
536
},
537
globalIgnorePatterns: {
538
type: 'array',
539
items: { type: 'string' }
540
}
541
},
542
additionalProperties: false,
543
required: ['mode']
544
}]
545
},
546
defaultOptions: [{
547
mode: 'strict',
548
overrides: {},
549
globalIgnorePatterns: []
550
}],
551
create(context, [options]) {
552
// Access deeply merged options with full type safety
553
const { mode, overrides, globalIgnorePatterns } = options;
554
555
function getEffectiveOptions(fileName: string) {
556
// Check overrides
557
for (const [pattern, override] of Object.entries(overrides ?? {})) {
558
if (new RegExp(pattern).test(fileName)) {
559
return {
560
mode: override.mode ?? mode,
561
ignore: override.ignore ?? false
562
};
563
}
564
}
565
566
return { mode, ignore: false };
567
}
568
569
return {
570
Program() {
571
const fileName = context.getFilename();
572
const effectiveOptions = getEffectiveOptions(fileName);
573
574
if (effectiveOptions.ignore) return;
575
576
// Rule logic based on effective options
577
}
578
};
579
}
580
});
581
```
582
583
## Utility Composition Example
584
585
```typescript { .api }
586
import { ESLintUtils, ASTUtils, TSESLint } from '@typescript-eslint/utils';
587
588
const createRule = ESLintUtils.RuleCreator(name => `https://example.com/${name}`);
589
590
export default createRule({
591
name: 'comprehensive-example',
592
meta: {
593
type: 'suggestion',
594
docs: { description: 'Demonstrates ESLintUtils composition' },
595
messages: {
596
issue: 'Issue detected: {{description}}'
597
},
598
schema: []
599
},
600
defaultOptions: [],
601
create(context) {
602
// Combine multiple utilities
603
const services = ESLintUtils.getParserServices(context, true);
604
const sourceCode = context.getSourceCode();
605
606
return {
607
CallExpression(node) {
608
// Use parser services if available
609
if (services.hasFullTypeInformation) {
610
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
611
const type = services.program!.getTypeChecker().getTypeAtLocation(tsNode);
612
}
613
614
// Use AST utilities
615
if (ASTUtils.isOptionalCallExpression(node)) {
616
context.report({
617
node,
618
messageId: 'issue',
619
data: { description: 'optional call detected' }
620
});
621
}
622
623
// Use null safety
624
const parent = ESLintUtils.nullThrows(
625
node.parent,
626
ESLintUtils.NullThrowsReasons.MissingParent
627
);
628
}
629
};
630
}
631
});
632
```