TypeScript
This page curates a list of example ast-grep rules to check and to rewrite TypeScript applications. Check out the Repository of ESLint rules recreated with ast-grep.
TypeScript and TSX are different.
TypeScript is a typed JavaScript extension and TSX is a further extension that allows JSX elements. They need different parsers because of conflicting syntax.
However, you can use the languageGlobs
option to force ast-grep to use parse .ts
files as TSX.
Find Import File without Extension
Description
In ECMAScript modules (ESM), the module specifier must include the file extension, such as .js
or .mjs
, when importing local or absolute modules. This is because ESM does not perform any automatic file extension resolution, unlike CommonJS modules tools such as Webpack or Babel. This behavior matches how import behaves in browser environments, and is specified by the ESM module spec.
The rule finds all imports (static and dynamic) for files without a file extension.
YAML
id: find-import-file
language: js
rule:
regex: "/[^.]+[^/]$"
kind: string_fragment
any:
- inside:
stopBy: end
kind: import_statement
- inside:
stopBy: end
kind: call_expression
has:
field: function
regex: "^import$"
Example
import a, {b, c, d} from "./file";
import e from "./other_file.js";
import "./folder/";
import {x} from "package";
import {y} from "package/with/path";
import("./dynamic1");
import("./dynamic2.js");
my_func("./unrelated_path_string")
Contributed by
DasSurma in this tweet.
Migrate XState to v5 from v4 Has Fix
Description
XState is a state management/orchestration library based on state machines, statecharts, and the actor model. It allows you to model complex logic in event-driven ways, and orchestrate the behavior of many actors communicating with each other.
XState's v5 version introduced some breaking changes and new features compared to v4. While the migration should be a straightforward process, it is a tedious process and requires knowledge of the differences between v4 and v5.
ast-grep provides a way to automate the process and a way to encode valuable knowledge to executable rules.
The following example picks up some migration items and demonstrates the power of ast-grep's rule system.
YAML
The rules below correspond to XState v5's createMachine
, createActor
, and machine.provide
.
The example shows how ast-grep can use various features like utility rule, transformation and multiple rule in single file to automate the migration. Each rule has a clear and descriptive id
field that explains its purpose.
For more information, you can use Codemod AI to provide more detailed explanation for each rule.
id: migrate-import-name
utils:
FROM_XS: {kind: import_statement, has: {kind: string, regex: xstate}}
XS_EXPORT:
kind: identifier
inside: { has: { matches: FROM_XS }, stopBy: end }
rule: { regex: ^Machine|interpret$, pattern: $IMPT, matches: XS_EXPORT }
transform:
STEP1:
replace: {by: create$1, replace: (Machine), source: $IMPT }
FINAL:
replace: { by: createActor, replace: interpret, source: $STEP1 }
fix: $FINAL
---
id: migrate-to-provide
rule: { pattern: $MACHINE.withConfig }
fix: $MACHINE.provide
---
id: migrate-to-actors
rule:
kind: property_identifier
regex: ^services$
inside: { pattern: $M.withConfig($$$ARGS), stopBy: end }
fix: actors
Example
import { Machine, interpret } from 'xstate';
const machine = Machine({ /*...*/});
const specificMachine = machine.withConfig({
actions: { /* ... */ },
guards: { /* ... */ },
services: { /* ... */ },
});
const actor = interpret(specificMachine, {
/* actor options */
});
Diff
import { Machine, interpret } from 'xstate';
import { createMachine, createActor } from 'xstate';
const machine = Machine({ /*...*/});
const machine = createMachine({ /*...*/});
const specificMachine = machine.withConfig({
const specificMachine = machine.provide({
actions: { /* ... */ },
guards: { /* ... */ },
services: { /* ... */ },
actors: { /* ... */ },
});
const actor = interpret(specificMachine, {
const actor = createActor(specificMachine, {
/* actor options */
});
Contributed by
Inspired by XState's blog.
No await
in Promise.all
array Has Fix
Description
Using await
inside an inline Promise.all
array is usually a mistake, as it defeats the purpose of running the promises in parallel. Instead, the promises should be created without await
and passed to Promise.all
, which can then be awaited.
YAML
id: no-await-in-promise-all
language: typescript
rule:
pattern: await $A
inside:
pattern: Promise.all($_)
stopBy:
not: { any: [{kind: array}, {kind: arguments}] }
fix: $A
Example
const [foo, bar] = await Promise.all([
await getFoo(),
getBar(),
(async () => { await getBaz()})(),
])
Diff
const [foo, bar] = await Promise.all([
await getFoo(),
getFoo(),
getBar(),
(async () => { await getBaz()})(),
])
Contributed by
Inspired by Alvar Lagerlöf
No console
except in catch
block Has Fix
Description
Using console
methods is usually for debugging purposes and therefore not suitable to ship to the client. console
can expose sensitive information, clutter the output, or affect the performance.
The only exception is using console.error
to log errors in the catch block, which can be useful for debugging production.
YAML
id: no-console-except-error
language: typescript
rule:
any:
- pattern: console.error($$$)
not:
inside:
kind: catch_clause
stopBy: end
- pattern: console.$METHOD($$$)
constraints:
METHOD:
regex: 'log|debug|warn'
Example
console.debug('')
try {
console.log('hello')
} catch (e) {
console.error(e) // OK
}
Diff
console.debug('')
try {
console.log('hello')
} catch (e) {
console.error(e) // OK
}
Contributed by
Inspired by Jerry Mouse
Find Import Usage
Description
It is common to find the usage of an imported module in a codebase. This rule helps you to find the usage of an imported module in your codebase. The idea of this rule can be broken into several parts:
- Find the use of an identifier
$MOD
- To find the import, we first need to find the root file of which
$MOD
isinside
- The
program
filehas
animport
statement - The
import
statementhas
the identifier$MOD
YAML
id: find-import-usage
language: typescript
rule:
kind: identifier # ast-grep requires a kind
pattern: $MOD # the identifier to find
inside: # find the root
stopBy: end
kind: program
has: # and has the import statement
kind: import_statement
has: # look for the matching identifier
stopBy: end
kind: import_specifier
pattern: $MOD # same pattern as the usage is enforced here
Example
import { MongoClient } from 'mongodb';
const url = 'mongodb://localhost:27017';
async function run() {
const client = new MongoClient(url);
}
Contributed by
Switch Chai from should
style to expect
Has Fix
Description
Chai is a BDD / TDD assertion library for JavaScript. It comes with two styles of assertions: should
and expect
.
The expect
interface provides a function as a starting point for chaining your language assertions and works with undefined
and null
values. The should
style allows for the same chainable assertions as the expect interface, however it extends each object with a should property to start your chain and does not work with undefined
and null
values.
This rule migrates Chai should
style assertions to expect
style assertions. Note this is an example rule and a excerpt from the original rules.
YAML
id: should_to_expect_instanceof
language: TypeScript
rule:
any:
- pattern: $NAME.should.be.an.instanceof($TYPE)
- pattern: $NAME.should.be.an.instanceOf($TYPE)
fix: |-
expect($NAME).instanceOf($TYPE)
---
id: should_to_expect_genericShouldBe
language: TypeScript
rule:
pattern: $NAME.should.be.$PROP
fix: |-
expect($NAME).to.be.$PROP
Example
it('should produce an instance of chokidar.FSWatcher', () => {
watcher.should.be.an.instanceof(chokidar.FSWatcher);
});
it('should expose public API methods', () => {
watcher.on.should.be.a('function');
watcher.emit.should.be.a('function');
watcher.add.should.be.a('function');
watcher.close.should.be.a('function');
watcher.getWatched.should.be.a('function');
});
Diff
it('should produce an instance of chokidar.FSWatcher', () => {
watcher.should.be.an.instanceof(chokidar.FSWatcher);
expect(watcher).instanceOf(chokidar.FSWatcher);
});
it('should expose public API methods', () => {
watcher.on.should.be.a('function');
watcher.emit.should.be.a('function');
watcher.add.should.be.a('function');
watcher.close.should.be.a('function');
watcher.getWatched.should.be.a('function');
expect(watcher.on).to.be.a('function');
expect(watcher.emit).to.be.a('function');
expect(watcher.add).to.be.a('function');
expect(watcher.close).to.be.a('function');
expect(watcher.getWatched).to.be.a('function');
});
Contributed by
Exercise
Exercise left to the reader: can you write a rule to implement this migration to node:assert
?
Speed up Barrel Import Has Fix
Description
A barrel import is a way to consolidate the exports of multiple modules into a single convenient module that can be imported using a single import statement. For instance, import {a, b, c} from './barrel'
.
It has some benefits to import each module directly from its own file without going through the barrel file. Such as reducing bundle size, improving building time or avoiding conflicting names.
YAML
id: speed-up-barrel-import
language: typescript
# find the barrel import statement
rule:
pattern: import {$$$IDENTS} from './barrel'
# rewrite imported identifiers to direct imports
rewriters:
- id: rewrite-identifer
rule:
pattern: $IDENT
kind: identifier
fix: import $IDENT from './barrel/$IDENT'
# apply the rewriter to the import statement
transform:
IMPORTS:
rewrite:
rewriters: [rewrite-identifer]
# $$$IDENTS contains imported identifiers
source: $$$IDENTS
# join the rewritten imports by newline
joinBy: "\n"
fix: $IMPORTS
Example
import {a, b, c} from './barrel'
Diff
import {a, b, c} from './barrel'
import a from './barrel/a'
import b from './barrel/b'
import c from './barrel/c'
Contributed by
Missing Component Decorator
Description
Angular lifecycle methods are a set of methods that allow you to hook into the lifecycle of an Angular component or directive. They must be used within a class that is decorated with the @Component()
decorator.
YAML
This rule illustrates how to use custom labels to highlight specific parts of the code.
id: missing-component-decorator
message: You're using an Angular lifecycle method, but missing an Angular @Component() decorator.
language: TypeScript
severity: warning
rule:
pattern:
context: 'class Hi { $METHOD() { $$$_} }'
selector: method_definition
inside:
pattern: 'class $KLASS $$$_ { $$$_ }'
stopBy: end
not:
has:
pattern: '@Component($$$_)'
constraints:
METHOD:
regex: ngOnInit|ngOnDestroy
labels:
KLASS:
style: primary
message: "This class is missing the decorator."
METHOD:
style: secondary
message: "This is an Angular lifecycle method."
metadata:
contributedBy: samwightt
Example
class NotComponent {
ngOnInit() {}
}
@Component()
class Klass {
ngOnInit() {}
}
Contributed by
Find Import Identifiers
Description
Finding import metadata can be useful. Below is a comprehensive snippet for extracting identifiers from various import statements:
- Alias Imports (
import { hello as world } from './file'
) - Default & Regular Imports (
import test from './my-test
') - Dynamic Imports (
require(...)
, andimport(...)
) - Side Effect & Namespace Imports (
import * as myCode from './code
')
YAML
# find-all-imports-and-identifiers.yaml
id: find-all-imports-and-identifiers
language: TypeScript
rule:
any:
# ALIAS IMPORTS
# ------------------------------------------------------------
# import { ORIGINAL as ALIAS } from 'SOURCE'
# ------------------------------------------------------------
- all:
# 1. Target the specific node type for named imports
- kind: import_specifier
# 2. Ensure it *has* an 'alias' field, capturing the alias identifier
- has:
field: alias
pattern: $ALIAS
# 3. Capture the original identifier (which has the 'name' field)
- has:
field: name
pattern: $ORIGINAL
# 4. Find an ANCESTOR import_statement and capture its source path
- inside:
stopBy: end # <<<--- Search ancestors.
kind: import_statement
has: # Ensure the found import_statement has the source field
field: source
pattern: $SOURCE
# DEFAULT IMPORTS
# ------------------------------------------------------------
# import { ORIGINAL } from 'SOURCE'
# ------------------------------------------------------------
- all:
- kind: import_statement
- has:
# Ensure it has an import_clause...
kind: import_clause
has:
# ...that directly contains an identifier (the default import name)
# This identifier is NOT under a 'named_imports' or 'namespace_import' node
kind: identifier
pattern: $DEFAULT_NAME
- has:
field: source
pattern: $SOURCE
# REGULAR IMPORTS
# ------------------------------------------------------------
# import { ORIGINAL } from 'SOURCE'
# ------------------------------------------------------------
- all:
# 1. Target the specific node type for named imports
- kind: import_specifier
# 2. Ensure it *has* an 'alias' field, capturing the alias identifier
- has:
field: name
pattern: $ORIGINAL
# 4. Find an ANCESTOR import_statement and capture its source path
- inside:
stopBy: end # <<<--- This is the key fix! Search ancestors.
kind: import_statement
has: # Ensure the found import_statement has the source field
field: source
pattern: $SOURCE
# DYNAMIC IMPORTS (Single Variable Assignment)
# ------------------------------------------------------------
# const VAR_NAME = require('SOURCE')
# ------------------------------------------------------------
- all:
- kind: variable_declarator
- has:
field: name
kind: identifier
pattern: $VAR_NAME # Capture the single variable name
- has:
field: value
any:
# Direct call
- all: # Wrap conditions in all
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
# Awaited call
- kind: await_expression
has:
all: # Wrap conditions in all
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
# DYNAMIC IMPORTS (Destructured Shorthand Assignment)
# ------------------------------------------------------------
# const { ORIGINAL } = require('SOURCE')
# ------------------------------------------------------------
- all:
# 1. Target the shorthand identifier within the pattern
- kind: shorthand_property_identifier_pattern
- pattern: $ORIGINAL
# 2. Ensure it's inside an object_pattern that is the name of a variable_declarator
- inside:
kind: object_pattern
inside: # Check the variable_declarator it belongs to
kind: variable_declarator
# 3. Check the value assigned by the variable_declarator
has:
field: value
any:
# Direct call
- all:
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
# Awaited call
- kind: await_expression
has:
all:
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
stopBy: end # Search ancestors to find the correct variable_declarator
# DYNAMIC IMPORTS (Destructured Alias Assignment)
# ------------------------------------------------------------
# const { ORIGINAL: ALIAS } = require('SOURCE')
# ------------------------------------------------------------
- all:
# 1. Target the pair_pattern for aliased destructuring
- kind: pair_pattern
# 2. Capture the original identifier (key)
- has:
field: key
kind: property_identifier # Could be string/number literal too, but property_identifier is common
pattern: $ORIGINAL
# 3. Capture the alias identifier (value)
- has:
field: value
kind: identifier
pattern: $ALIAS
# 4. Ensure it's inside an object_pattern that is the name of a variable_declarator
- inside:
kind: object_pattern
inside: # Check the variable_declarator it belongs to
kind: variable_declarator
# 5. Check the value assigned by the variable_declarator
has:
field: value
any:
# Direct call
- all:
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
# Awaited call
- kind: await_expression
has:
all:
- kind: call_expression
- has: { field: function, regex: '^(require|import)$' }
- has: { field: arguments, has: { kind: string, pattern: $SOURCE } } # Capture source
stopBy: end # Search ancestors to find the correct variable_declarator
stopBy: end # Ensure we check ancestors for the variable_declarator
# DYNAMIC IMPORTS (Side Effect / Source Only)
# ------------------------------------------------------------
# require('SOURCE')
# ------------------------------------------------------------
- all:
- kind: string # Target the source string literal directly
- pattern: $SOURCE
- inside: # String must be the argument of require() or import()
kind: arguments
parent:
kind: call_expression
has:
field: function
# Match 'require' identifier or 'import' keyword used dynamically
regex: '^(require|import)$'
stopBy: end # Search ancestors if needed (for the arguments/call_expression)
- not:
inside:
kind: lexical_declaration
stopBy: end # Search all ancestors up to the root
# NAMESPACE IMPORTS
# ------------------------------------------------------------
# import * as ns from 'mod'
# ------------------------------------------------------------
- all:
- kind: import_statement
- has:
kind: import_clause
has:
kind: namespace_import
has:
# namespace_import's child identifier is the alias
kind: identifier
pattern: $NAMESPACE_ALIAS
- has:
field: source
pattern: $SOURCE
# SIDE EFFECT IMPORTS
# ------------------------------------------------------------
# import 'mod'
# ------------------------------------------------------------
- all:
- kind: import_statement
- not: # Must NOT have an import_clause
has: { kind: import_clause }
- has: # But must have a source
field: source
pattern: $SOURCE
Example
//@ts-nocheck
// Named import
import { testing } from './tests';
// Aliased import
import { testing as test } from './tests2';
// Default import
import hello from 'hello_world1';
// Namespace import
import * as something from 'hello_world2';
// Side-effect import
import '@fastify/static';
// Type import
import {type hello1243 as testing} from 'hello';
// Require patterns
const mod = require('some-module');
require('polyfill');
// Destructured require
const { test122, test2 } = require('./destructured1');
// Aliased require
const { test122: test123, test2: test23, test3: test33 } = require('./destructured2');
// Mixed imports
import defaultExport, { namedExport } from './mixed';
import defaultExport2, * as namespace from './mixed2';
// Multiple import lines from the same file
import { one, two as alias, three } from './multiple';
import { never, gonna, give, you, up } from './multiple';
// String literal variations
import { test1 } from "./double-quoted";
import { test2 } from './single-quoted';
// Multiline imports
import {
longImport1,
longImport2 as alias2,
longImport3
} from './multiline';
// Dynamic imports
const dynamicModule = import('./dynamic1');
const {testing, testing123} = import('./dynamic2');
const asyncDynamicModule = await import('./async_dynamic1').then(module => module.default);
// Aliased dynamic import
const { originalIdentifier: aliasedDynamicImport} = await import('./async_dynamic2');
// Comments in imports
import /* test */ {
// Comment in import
commentedImport
} from './commented'; // End of line comment