esupgrade
v2025.14.2
Published
Auto-upgrade your JavaScript syntax
Downloads
2,709
Maintainers
Readme
esupgrade

Keeping your JavaScript and TypeScript code up to date with full browser compatibility.
Usage
esupgrade is safe and meant to be used automatically on your codebase. We recommend integrating it into your development workflow using pre-commit.
pre-commit
uvx pre-commit install# .pre-commit-config.yaml
repos:
- repo: https://github.com/codingjoe/esupgrade
rev: 2025.0.2 # Use the latest version
hooks:
- id: esupgradepre-commit run esupgrade --all-filesCLI
npx esupgrade --helpBrowser Support & Baseline
All transformations are based on Web Platform Baseline features. Baseline tracks which web platform features are safe to use across browsers.
By default, esupgrade uses widely available features, meaning they work in all major browsers (Chrome, Edge, Safari, Firefox) for at least 30 months. This ensures full compatibility while keeping your code modern.
You can opt into newly available features (available in all browsers for 0-30 months) with:
npx esupgrade --baseline newly-available <files>For more information about Baseline browser support, visit web.dev/baseline.
Supported File Types & Languages
.js- JavaScript.jsx- React/JSX.ts- TypeScript.tsx- TypeScript with JSX.mjs- ES Modules.cjs- CommonJS
Transformations
Widely available
var → const & let
-var x = 1;
-var y = 2;
-y = 3;
+const x = 1;
+let y = 2;
+y = 3;String concatenation → Template literals
-const greeting = 'Hello ' + name + '!';
-const message = 'You have ' + count + ' items';
+const greeting = `Hello ${name}!`;
+const message = `You have ${count} items`;Special handling for escape sequences and formatting:
Escape sequences:
\r(carriage return) is preserved, while\n(newline) is converted to actual newlines-const text = "Line 1\n" + "Line 2"; +const text = `Line 1 +Line 2`;Multiline concatenation: Visual structure is preserved with line continuation backslashes
-const longText = "First part " + - "second part"; +const longText = `First part \ +second part`;
Traditional for loops → for...of loops
-for (let i = 0; i < items.length; i++) {
- const item = items[i];
- console.log(item);
-}
+for (const item of items) {
+ console.log(item);
+}Transformations are limited to loops that start at 0, increment by 1, and where the index variable is not used in the loop body.
Array.from().forEach() → for...of loops
-Array.from(items).forEach(item => {
- console.log(item);
-});
+for (const item of items) {
+ console.log(item);
+}DOM forEach() → for...of loops
-document.querySelectorAll('.item').forEach(item => {
- item.classList.add('active');
-});
+for (const item of document.querySelectorAll('.item')) {
+ item.classList.add('active');
+}Supports:
document.querySelectorAll()document.getElementsByTagName()document.getElementsByClassName()document.getElementsByName()window.framesTransformations limited to inline arrow or function expressions with block statement bodies. Callbacks with index parameters or expression bodies are not transformed.
Array.from() → Array spread [...]
-const doubled = Array.from(numbers).map(n => n * 2);
-const filtered = Array.from(items).filter(x => x > 5);
-const arr = Array.from(iterable);
+const doubled = [...numbers].map(n => n * 2);
+const filtered = [...items].filter(x => x > 5);
+const arr = [...iterable];Array.from() with a mapping function or thisArg is not converted.
Object.assign({}, ...) → Object spread {...}
-const obj = Object.assign({}, obj1, obj2);
-const copy = Object.assign({}, original);
+const obj = { ...obj1, ...obj2 };
+const copy = { ...original };[!NOTE] TypeScript does not support generic object spread yet: https://github.com/Microsoft/TypeScript/issues/10727 You might need to manually adjust the type after transformation:
const object_with_generic_type: object = { ...(myGenericObject as object) }
Array.concat() → Array spread [...]
-const combined = arr1.concat(arr2, arr3);
-const withItem = array.concat([item]);
+const combined = [...arr1, ...arr2, ...arr3];
+const withItem = [...array, item];Array.slice(0) → Array spread [...]
-const copy = [1, 2, 3].slice(0);
-const clone = Array.from(items).slice();
+const copy = [...[1, 2, 3]];
+const clone = [...Array.from(items)];Math.pow() → Exponentiation operator **
-const result = Math.pow(2, 3);
-const area = Math.PI * Math.pow(radius, 2);
+const result = 2 ** 3;
+const area = Math.PI * radius ** 2;Named function assignments → Function declarations
-const myFunc = () => { return 42; };
-const add = (a, b) => a + b;
-const greet = function(name) { return "Hello " + name; };
+function myFunc() { return 42; }
+function add(a, b) { return a + b; }
+function greet(name) { return "Hello " + name; }Transforms arrow functions and anonymous function expressions assigned to variables into proper named function declarations. This provides better structure and semantics for top-level functions.
Functions using this or arguments are not converted to preserve semantics.
TypeScript parameter and return type annotations are preserved:
-let myAdd = function (x: number, y: number): number {
- return x + y;
-};
+function myAdd(x: number, y: number): number {
+ return x + y;
+}Generic type parameters are also preserved:
-export const useHook = <T extends object>(props: T): T => {
- return props;
-};
+export function useHook<T extends object>(props: T): T {
+ return props;
+}Variables with TypeScript type annotations but no function return type are skipped:
// Not transformed - variable type annotation cannot be transferred
const Template: StoryFn<MyType> = () => { return <div>Hello</div>; };Anonymous function expressions → Arrow functions
-items.map(function(item) { return item.name; });
-button.addEventListener('click', function(event) { process(event); });
+items.map(item => { return item.name; });
+button.addEventListener('click', event => { process(event); });Anonymous function expressions not in variable declarations (like callbacks and event handlers) are converted to arrow functions.
Functions using this, arguments, or super are not converted to preserve semantics.
Constructor functions → Classes
-function Person(name, age) {
- this.name = name;
- this.age = age;
-}
-
-Person.prototype.greet = function() {
- return 'Hello, I am ' + this.name;
-};
-
-Person.prototype.getAge = function() {
- return this.age;
-};
+class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ greet() {
+ return 'Hello, I am ' + this.name;
+ }
+
+ getAge() {
+ return this.age;
+ }
+}Transforms constructor functions (both function declarations and variable declarations) that meet these criteria:
- Function name starts with an uppercase letter
- At least one prototype method is defined
- Prototype methods using
thisin arrow functions are skipped - Prototype object literals with getters, setters, or computed properties are skipped
console.log() → console.info()
-console.log('User logged in:', username);
-console.log({ userId, action: 'login' });
+console.info('User logged in:', username);
+console.info({ userId, action: 'login' });While console.log and console.info are functionally identical in browsers.
This transformation provides semantic clarity by using an explicit log level, but review your logging infrastructure before applying.
Remove redundant 'use strict' from modules
-'use strict';
import { helper } from './utils';
export function main() {
return helper();
}ES6 modules are automatically in strict mode, making explicit 'use strict' directives redundant. This transformation applies to files with import or export statements.
Global context → globalThis
-const global = window;
-const loc = window.location.href;
+const global = globalThis;
+const loc = globalThis.location.href;-const global = self;
-const nav = self.navigator;
+const global = globalThis;
+const nav = globalThis.navigator;-const global = Function('return this')();
+const global = globalThis;Null/undefined checks → Nullish coalescing operator (??)
-const value = x !== null && x !== undefined ? x : defaultValue;
+const value = x ?? defaultValue;-const result = obj.prop !== null && obj.prop !== undefined ? obj.prop : 0;
+const result = obj.prop ?? 0;indexOf() → includes()
-const found = [1, 2, 3].indexOf(item) !== -1;
-const exists = "hello".indexOf(substr) > -1;
-const hasValue = ["a", "b", "c"].indexOf(value) >= 0;
+const found = [1, 2, 3].includes(item);
+const exists = "hello".includes(substr);
+const hasValue = ["a", "b", "c"].includes(value);-if ([1, 2, 3].indexOf(item) === -1) {
- console.log('not found');
-}
+if (![1, 2, 3].includes(item)) {
+ console.log('not found');
+}Transforms indexOf() calls with a single argument (search value) when it can statically verify that the receiver is an array or string (for example, array literals, string literals, or safe method chains).
Calls with a fromIndex parameter are not transformed as they have different semantics than includes(). As a result, patterns such as [1, 2, 3].indexOf(item) !== -1 are upgraded, while arr.indexOf(item) !== -1 may be left unchanged if the transformer cannot prove that arr is an array.
String.substr() → String.slice()
-const result = "hello world".substr(0, 5);
-const end = "example".substr(3);
+const result = "hello world".slice(0, 0 + 5);
+const end = "example".slice(3);Transforms the deprecated substr() method to slice():
str.substr(start, length)becomesstr.slice(start, start + length)str.substr(start)becomesstr.slice(start)str.substr()becomesstr.slice()
Transformations are limited to when the receiver can be verified as a string (string literals, template literals, or string method chains).
Object.keys().forEach() → Object.entries()
-Object.keys(obj).forEach(key => {
- const value = obj[key];
- console.log(key, value);
-});
+Object.entries(obj).forEach(([key, value]) => {
+ console.log(key, value);
+});Transforms Object.keys() iteration patterns where the value is accessed from the same object into Object.entries() with array destructuring. This eliminates duplicate property lookups and makes the code more concise.
Transforms when:
- The callback has one parameter (the key)
- The first statement in the callback assigns
obj[key]to a variable - The object being accessed matches the object passed to Object.keys()
indexOf() prefix check → String.startsWith()
-const isPrefix = "hello world".indexOf("hello") === 0;
-const notPrefix = str.indexOf(prefix) !== 0;
+const isPrefix = "hello world".startsWith("hello");
+const notPrefix = !str.startsWith(prefix);Transforms indexOf() prefix checks to the more explicit startsWith() method. Transforms when the receiver can be verified as a string and indexOf() is compared to 0.
substring() prefix check → String.startsWith()
-const matches = "hello world".substring(0, prefix.length) === prefix;
-const noMatch = str.substring(0, prefix.length) !== prefix;
+const matches = "hello world".startsWith(prefix);
+const noMatch = !str.startsWith(prefix);Transforms substring() prefix comparisons to startsWith(). Transforms patterns where substring(0, prefix.length) is compared to prefix.
lastIndexOf() suffix check → String.endsWith()
-const isSuffix = str.lastIndexOf(suffix) === str.length - suffix.length;
-const notSuffix = "hello world".lastIndexOf("world") !== "hello world".length - "world".length;
+const isSuffix = str.endsWith(suffix);
+const notSuffix = !"hello world".endsWith("world");Transforms lastIndexOf() suffix checks to the more explicit endsWith() method. Transforms when the receiver can be verified as a string and the pattern matches lastIndexOf(suffix) === str.length - suffix.length.
arguments object → Rest parameters ...
-function fn() {
- const args = Array.from(arguments);
- // use args
-}
+function fn(...args) {
+ // use args
+}-function fn() {
- const args = [].slice.call(arguments);
- // use args
-}
+function fn(...args) {
+ // use args
+}Transforms the arguments object to rest parameters when:
- Function is a regular function (not arrow function)
- Function doesn't already have rest parameters
argumentsis used in the conversion pattern (Array.from(arguments)or[].slice.call(arguments))argumentsis not used elsewhere in the function
The transformer handles cases where Array.from(arguments) has already been converted to [...arguments] by other transformers.
Manual default values → Default parameters
-function fn(x) {
- if (x === undefined) x = defaultValue;
- // use x
-}
+function fn(x = defaultValue) {
+ // use x
+}Note: The x = x || defaultValue pattern is NOT transformed as it has different semantics (triggers on any falsy value, instead of undefined).
Promise chains → async/await
-function getData() {
- return fetch('/api/data')
- .then(result => {
- // handle result
- })
- .catch(err => {
- // handle error
- });
-}
+async function getData() {
+ try {
+ const result = await fetch('/api/data');
+ // handle result
+ } catch (err) {
+ // handle error
+ }
+}- The promise chain is returned from the function or used inside an already async function
- The expression is a known promise (
fetch(),new Promise(), or promise methods)
Newly available
These transformations are mainly to harden code for future releases and should be used with caution.
new Promise((resolve) => { ... }) → Promise.try
-new Promise((resolve) => {
- const result = doSomething();
- resolve(result);
-});
+Promise.try(() => {
+ return doSomething();
+});Versioning
esupgrade uses the calver YYYY.MINOR.PATCH versioning scheme.
The year indicates the baseline version. New transformations are added in minor releases, while patches are reserved for bug fixes.
Related Projects
Thanks to these projects for inspiring esupgrade:
- @asottile's pyupgrade for Python
- @adamchainz' django-upgrade for Django
Distinction
lebab is a similar project that focuses on ECMAScript 6+ transformations without considering browser support. esupgrade is distinct in that it applies transformations that are safe based on Baseline browser support. Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is limited to JavaScript.
