or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ast-utils.mdeslint-utils.mdindex.mdjson-schema.mdts-eslint.mdts-utils.md

eslint-utils.mddocs/

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

```