DEV Community

Cover image for Upcoming JavaScript Features You Should Know About
Max Prilutskiy
Max Prilutskiy

Posted on

84 10 10 12 8

Upcoming JavaScript Features You Should Know About

After 20 years of writing JavaScript, I've seen many changes - from callback hell to async/await. But the upcoming JavaScript features will transform how we write code completely.

We've tested these proposals using transpilers and polyfills, ans the results are impressive: code that took 30 lines now takes 10, complex logic becomes readable at a glance, and even junior developers (and Devin 😅) can understand complex parts of our codebase.

If you've read my previous articles on HTML5 elements you didn't know you need or CSS modal windows, you know we like unusual tech at Lingo.dev. These upcoming JavaScript features solve real problems that have annoyed developers for years.

Pipeline Operator Improves Code Readability

Complex data transformations in JavaScript often result in deeply nested function calls that are difficult to read and maintain. Developers must trace through nested functions from inside out, jumping between parentheses to understand the flow of data.

The pipeline operator (|>) solves this problem by allowing data to flow through a series of operations in a clear, top-to-bottom manner:

// Instead of this nested mess
const result = saveToDatabase(
  validateUser(
    normalizeData(
      enrichUserProfile(user)
    )
  )
);

// You'll write this
const result = user
  |> enrichUserProfile
  |> normalizeData
  |> validateUser
  |> saveToDatabase;
Enter fullscreen mode Exit fullscreen mode

Let's look at a more complex real-world example. Consider an image processing service that has grown over time:

// Before: Nested function hell
function processImage(image) {
  return compressImage(
    addWatermark(
      optimizeForWeb(
        applyFilter(
          resizeImage(image, { width: 800, height: 600 }),
          'sepia'
        )
      ),
      'Copyright 2025'
    ),
    0.8
  );
}

// After: Clean, readable pipeline
function processImage(image) {
  return image
    |> (img) => resizeImage(img, { width: 800, height: 600 })
    |> (img) => applyFilter(img, 'sepia')
    |> optimizeForWeb
    |> (img) => addWatermark(img, 'Copyright 2025')
    |> (img) => compressImage(img, 0.8);
}
Enter fullscreen mode Exit fullscreen mode

Here's another practical example showing how the pipeline operator simplifies data processing for analytics:

// Before: Hard to follow the data flow
function analyzeUserData(users) {
  return generateReport(
    groupByMetric(
      filterInactiveUsers(
        normalizeUserData(users)
      ),
      'registrationMonth'
    )
  );
}

// After: Clear data transformation steps
function analyzeUserData(users) {
  return users
    |> normalizeUserData
    |> filterInactiveUsers
    |> (data) => groupByMetric(data, 'registrationMonth')
    |> generateReport;
}
Enter fullscreen mode Exit fullscreen mode

The pipeline operator also makes it easier to insert debugging or logging between steps:

function processPayment(payment) {
  return payment
    |> validatePayment
    |> (result) => {
        console.log(`Validation result: ${JSON.stringify(result)}`);
        return result;
      }
    |> processTransaction
    |> (result) => {
        console.log(`Transaction result: ${JSON.stringify(result)}`);
        return result;
      }
    |> sendReceipt;
}
Enter fullscreen mode Exit fullscreen mode

How to Use It Today

The pipeline operator is currently at Stage 2 in the TC39 process as of May 2025. While not yet part of the official JavaScript specification, you can start using it today:

With Babel (Generic Setup):

# Install the pipeline operator plugin
npm install --save-dev @babel/plugin-proposal-pipeline-operator
Enter fullscreen mode Exit fullscreen mode

Add to your .babelrc:

{
  "plugins": [
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "hack", "topicToken": "%"}]
  ]
}
Enter fullscreen mode Exit fullscreen mode

With Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
Enter fullscreen mode Exit fullscreen mode
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
        ]
      }
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

With Next.js:

# Install the pipeline operator plugin
npm install --save-dev @babel/plugin-proposal-pipeline-operator
Enter fullscreen mode Exit fullscreen mode
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "hack", "topicToken": "%" }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

With tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-pipeline-operator
Enter fullscreen mode Exit fullscreen mode
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

With Remix (using Vite):

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
Enter fullscreen mode Exit fullscreen mode
// vite.config.js
import { defineConfig } from 'vite';
import { vitePlugin as remix } from '@remix-run/dev';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-pipeline-operator', { proposal: 'hack', topicToken: '%' }]
        ]
      }
    }),
    remix()
  ]
});
Enter fullscreen mode Exit fullscreen mode

Pattern Matching Simplifies Complex Conditionals

Complex if/else statements and switch cases quickly become unwieldy in large codebases. Functions with nested if/else blocks checking various object properties make it nearly impossible to verify all edge cases.

Pattern matching provides a direct solution to this problem.

This feature brings functional programming capabilities to JavaScript, allowing you to match and destructure complex data in a single operation:

function processMessage(message) {
  return match (message) {
    when String(text) => `Text message: ${text}`,
    when [String(sender), String(content)] => `Message from ${sender}: ${content}`,
    when { type: 'error', content: String(text), code: Number(code) } => 
      `Error: ${text} (Code: ${code})`,
    when _ => 'Unknown message format'
  };
}
Enter fullscreen mode Exit fullscreen mode

Without pattern matching, you'd need multiple if/else statements with type checking:

function processMessage(message) {
  // Check if it's a string
  if (typeof message === 'string') {
    return `Text message: ${message}`;
  }

  // Check if it's an array with specific structure
  if (Array.isArray(message) && 
      message.length === 2 && 
      typeof message[0] === 'string' && 
      typeof message[1] === 'string') {
    return `Message from ${message[0]}: ${message[1]}`;
  }

  // Check if it's an object with specific properties
  if (message && 
      typeof message === 'object' && 
      message.type === 'error' && 
      typeof message.content === 'string' && 
      typeof message.code === 'number') {
    return `Error: ${message.content} (Code: ${message.code})`;
  }

  // Default case
  return 'Unknown message format';
}
Enter fullscreen mode Exit fullscreen mode

Pattern matching excels when handling complex state transitions in modern web applications:

function handleUserAction(state, action) {
  return match ([state, action]) {
    when [{ status: 'idle' }, { type: 'FETCH_START' }] => 
      ({ status: 'loading', data: state.data }),

    when [{ status: 'loading' }, { type: 'FETCH_SUCCESS', payload }] => 
      ({ status: 'success', data: payload, error: null }),

    when [{ status: 'loading' }, { type: 'FETCH_ERROR', error }] => 
      ({ status: 'error', data: null, error }),

    when [{ status: 'success' }, { type: 'REFRESH' }] => 
      ({ status: 'loading', data: state.data }),

    when _ => state
  };
}
Enter fullscreen mode Exit fullscreen mode

The equivalent code without pattern matching is significantly more verbose:

function handleUserAction(state, action) {
  // Check idle state + fetch start
  if (state.status === 'idle' && action.type === 'FETCH_START') {
    return { status: 'loading', data: state.data };
  }

  // Check loading state + fetch success
  if (state.status === 'loading' && action.type === 'FETCH_SUCCESS') {
    return { status: 'success', data: action.payload, error: null };
  }

  // Check loading state + fetch error
  if (state.status === 'loading' && action.type === 'FETCH_ERROR') {
    return { status: 'error', data: null, error: action.error };
  }

  // Check success state + refresh
  if (state.status === 'success' && action.type === 'REFRESH') {
    return { status: 'loading', data: state.data };
  }

  // Default: return unchanged state
  return state;
}
Enter fullscreen mode Exit fullscreen mode

Pattern matching also provides exhaustiveness checking - the compiler warns you if you've missed handling a possible case. This eliminates an entire class of bugs that plague traditional conditional logic.

Here's another practical example for parsing configuration formats:

function parseConfig(config) {
  return match (config) {
    when { version: 1, settings: Object(settings) } => 
      parseV1Settings(settings),

    when { version: 2, config: Object(settings) } => 
      parseV2Settings(settings),

    when String(jsonString) => 
      parseConfig(JSON.parse(jsonString)),

    when [String(env), Object(overrides)] =>
      mergeConfigs(getEnvConfig(env), overrides),

    when _ => 
      throw new Error(`Invalid configuration format: ${JSON.stringify(config)}`)
  };
}
Enter fullscreen mode Exit fullscreen mode

How to Use It Today

Pattern matching is currently at Stage 1 in the TC39 process as of May 2025, which means it's still in the proposal phase with ongoing discussions about syntax and semantics. However, you can experiment with it today:

With Babel (Generic Setup):

# Install the pattern matching plugin
npm install --save-dev babel-plugin-proposal-pattern-matching
Enter fullscreen mode Exit fullscreen mode

Add to your .babelrc:

{
  "plugins": ["babel-plugin-proposal-pattern-matching"]
}
Enter fullscreen mode Exit fullscreen mode

With Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core babel-plugin-proposal-pattern-matching
Enter fullscreen mode Exit fullscreen mode
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          'babel-plugin-proposal-pattern-matching'
        ]
      }
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

With Next.js:

# Install the pattern matching plugin
npm install --save-dev babel-plugin-proposal-pattern-matching
Enter fullscreen mode Exit fullscreen mode
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    "babel-plugin-proposal-pattern-matching"
  ]
}
Enter fullscreen mode Exit fullscreen mode

With tsup:

# Install dependencies
npm install --save-dev tsup @babel/core babel-plugin-proposal-pattern-matching
Enter fullscreen mode Exit fullscreen mode
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              'babel-plugin-proposal-pattern-matching'
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Production Alternatives:

For production code today, use the ts-pattern library:

npm install ts-pattern
Enter fullscreen mode Exit fullscreen mode
import { match, P } from 'ts-pattern';

function processMessage(message: unknown) {
  return match(message)
    .with(P.string, text => `Text message: ${text}`)
    .with([P.string, P.string], ([sender, content]) => 
      `Message from ${sender}: ${content}`)
    .with({ type: 'error', content: P.string, code: P.number }, 
      ({ content, code }) => `Error: ${content} (Code: ${code})`)
    .otherwise(() => 'Unknown message format');
}
Enter fullscreen mode Exit fullscreen mode

Temporal API Solves Date Handling Problems

JavaScript's built-in Date object has long been a source of frustration for developers. It's mutable (dates can be accidentally modified), has confusing month indexing (January is 0!), and timezone handling is problematic. These issues have led to the widespread use of libraries like Moment.js, date-fns, and Luxon.

The Temporal API provides a complete solution to these problems by reimagining date and time handling in JavaScript.

Here's a practical example of scheduling a meeting across timezones:

// Current approach with Date (likely to have bugs)
function scheduleMeeting(startDate, durationInMinutes, timeZone) {
  const start = new Date(startDate);
  const end = new Date(start.getTime() + durationInMinutes * 60000);

  return {
    start: start.toISOString(),
    end: end.toISOString(),
    timeZone: timeZone // Not actually used in calculations
  };
}

// With Temporal API
function scheduleMeeting(startDateTime, durationInMinutes, timeZone) {
  const start = Temporal.ZonedDateTime.from(startDateTime);
  const end = start.add({ minutes: durationInMinutes });

  return {
    start: start.toString(),
    end: end.toString(),
    timeZone: start.timeZoneId // Properly tracked
  };
}
Enter fullscreen mode Exit fullscreen mode

For international flight booking systems, calculating flight durations across timezones becomes straightforward:

// Current approach with Date (error-prone)
function calculateFlightDuration(departure, arrival, departureTimeZone, arrivalTimeZone) {
  // Convert to milliseconds and calculate difference
  const departureTime = new Date(departure);
  const arrivalTime = new Date(arrival);

  // This doesn't account for timezone differences correctly
  const durationMs = arrivalTime - departureTime;

  return {
    hours: Math.floor(durationMs / (1000 * 60 * 60)),
    minutes: Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)),
    // No easy way to get arrival in departure's timezone
  };
}

// With Temporal API
function calculateFlightDuration(departure, arrival) {
  const departureTime = Temporal.ZonedDateTime.from(departure);
  const arrivalTime = Temporal.ZonedDateTime.from(arrival);

  // Accurate duration calculation across time zones
  const duration = departureTime.until(arrivalTime);

  return {
    hours: duration.hours,
    minutes: duration.minutes,
    inLocalTime: arrivalTime.toLocaleString(),
    inDepartureTime: arrivalTime.withTimeZone(departureTime.timeZoneId).toLocaleString()
  };
}
Enter fullscreen mode Exit fullscreen mode

Here's another example showing how Temporal API handles recurring events, which are notoriously difficult with the current Date object:

// Current approach with Date (complex and error-prone)
function getNextMeetingDates(startDate, count) {
  const dates = [];
  const current = new Date(startDate);

  for (let i = 0; i < count; i++) {
    dates.push(new Date(current));

    // Add 2 weeks - error-prone due to month boundaries, DST changes, etc.
    current.setDate(current.getDate() + 14);
  }

  return dates;
}

// With Temporal API
function getNextMeetingDates(startDate, count) {
  const start = Temporal.PlainDate.from(startDate);
  const dates = [];

  for (let i = 0; i < count; i++) {
    const nextDate = start.add({ days: i * 14 });
    dates.push(nextDate);
  }

  return dates;
}
Enter fullscreen mode Exit fullscreen mode

The Temporal API provides several key advantages:

  1. Immutability: All Temporal objects are immutable, preventing accidental modifications
  2. Separate types for different use cases: PlainDate, PlainTime, ZonedDateTime, etc.
  3. Intuitive methods: Clear, chainable methods for date arithmetic
  4. Proper timezone handling: Built-in support for timezones and daylight saving time
  5. Consistent behavior: Works the same way across all browsers

How to Use It Today

The Temporal API is at Stage 3 in the TC39 process as of May 2025, which means it's nearing completion and implementations are being developed. Here's how to use it today:

With the Official Polyfill:

npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// Import in your code
import { Temporal } from '@js-temporal/polyfill';

// Get the current date and time
const now = Temporal.Now.plainDateTimeISO();
console.log(`Current time: ${now.toString()}`);
Enter fullscreen mode Exit fullscreen mode

With Vite:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// main.js or any entry file
import { Temporal } from '@js-temporal/polyfill';

// Now you can use Temporal API in your code
const now = Temporal.Now.plainDateTimeISO();
console.log(`Current time: ${now.toString()}`);
Enter fullscreen mode Exit fullscreen mode

With Next.js:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// pages/_app.js
import { Temporal } from '@js-temporal/polyfill';

// Make Temporal available globally if needed
if (typeof window !== 'undefined') {
  window.Temporal = Temporal;
}

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

With tsup:

# Install the Temporal API polyfill
npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// src/index.ts
import { Temporal } from '@js-temporal/polyfill';

// Export it if you want to make it available to consumers of your package
export { Temporal };

// Your other code here
Enter fullscreen mode Exit fullscreen mode

Browser Support:

  • Chrome/Edge: Available behind the "Experimental JavaScript" flag
  • Firefox: Available in Firefox 139 and later
  • Safari: Not yet implemented

To check if Temporal is natively supported:

if (typeof Temporal !== 'undefined') {
  console.log('Temporal API is natively supported');
} else {
  console.log('Using polyfill');
  // Import polyfill
}
Enter fullscreen mode Exit fullscreen mode

Resource Management Eliminates Memory Leaks

JavaScript has long lacked deterministic resource cleanup. When working with files, database connections, or hardware access, developers must manually ensure resources are properly released - even in error cases.

This limitation leads to memory leaks and resource exhaustion bugs. The typical pattern involves try/finally blocks that quickly become unwieldy:

async function processFile(path) {
  let file = null;
  try {
    file = await fs.promises.open(path, 'r');
    const content = await file.readFile({ encoding: 'utf8' });
    return processContent(content);
  } finally {
    if (file) {
      await file.close();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The new using and await using statements provide deterministic resource management in JavaScript:

async function processFile(path) {
  await using file = await fs.promises.open(path, 'r');
  const content = await file.readFile({ encoding: 'utf8' });
  return processContent(content);
  // File is automatically closed when the block exits
}
Enter fullscreen mode Exit fullscreen mode

Let's look at a more complex example with multiple resources that must be managed in a specific order:

// Current approach: Nested try/finally blocks
async function processData(dbConfig, filePath) {
  let db = null;
  let file = null;

  try {
    db = await Database.connect(dbConfig);

    try {
      file = await fs.promises.open(filePath, 'r');
      const data = await file.readFile({ encoding: 'utf8' });
      const processed = processRawData(data);

      return db.store(processed);
    } finally {
      if (file) {
        await file.close();
      }
    }
  } finally {
    if (db) {
      await db.disconnect();
    }
  }
}

// With resource management: Clean and safe
async function processData(dbConfig, filePath) {
  await using db = await Database.connect(dbConfig);
  await using file = await fs.promises.open(filePath, 'r');

  const data = await file.readFile({ encoding: 'utf8' });
  const processed = processRawData(data);

  return db.store(processed);
  // Resources are automatically cleaned up in reverse order:
  // 1. file is closed
  // 2. db is disconnected
}
Enter fullscreen mode Exit fullscreen mode

For database-heavy applications, this feature transforms connection pool management:

class DatabaseConnection {
  constructor(config) {
    this.config = config;
    this.connection = null;
  }

  async connect() {
    this.connection = await createConnection(this.config);
    return this;
  }

  async query(sql, params) {
    return this.connection.query(sql, params);
  }

  async [Symbol.asyncDispose]() {
    if (this.connection) {
      await this.connection.close();
      this.connection = null;
    }
  }
}

// Using the connection with automatic cleanup
async function getUserData(userId) {
  await using db = await new DatabaseConnection(config).connect();
  return db.query('SELECT * FROM users WHERE id = ?', [userId]);
  // Connection is automatically closed when the function exits
}
Enter fullscreen mode Exit fullscreen mode

Here's another example showing how to manage hardware resources like WebUSB devices:

// Current approach: Manual cleanup required
async function readFromUSBDevice(deviceFilter) {
  let device = null;
  try {
    const devices = await navigator.usb.getDevices();
    device = devices.find(d => d.productId === deviceFilter.productId);

    if (!device) {
      device = await navigator.usb.requestDevice({ filters: [deviceFilter] });
    }

    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(0);

    const result = await device.transferIn(1, 64);
    return new TextDecoder().decode(result.data);
  } finally {
    if (device) {
      try {
        await device.close();
      } catch (e) {
        console.error("Error closing device:", e);
      }
    }
  }
}

// With resource management: Automatic cleanup
class USBDeviceResource {
  constructor(device) {
    this.device = device;
  }

  static async create(deviceFilter) {
    const devices = await navigator.usb.getDevices();
    let device = devices.find(d => d.productId === deviceFilter.productId);

    if (!device) {
      device = await navigator.usb.requestDevice({ filters: [deviceFilter] });
    }

    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(0);

    return new USBDeviceResource(device);
  }

  async read() {
    const result = await this.device.transferIn(1, 64);
    return new TextDecoder().decode(result.data);
  }

  async [Symbol.asyncDispose]() {
    try {
      await this.device.close();
    } catch (e) {
      console.error("Error closing device:", e);
    }
  }
}

async function readFromUSBDevice(deviceFilter) {
  await using device = await USBDeviceResource.create(deviceFilter);
  return device.read();
  // Device is automatically closed when the function exits
}
Enter fullscreen mode Exit fullscreen mode

How to Use It Today

The Explicit Resource Management proposal (using/await using) is at Stage 3 in the TC39 process as of May 2025, which means it's nearing standardization. Here's how to use it today:

With Babel (Generic Setup):

# Install the resource management plugin
npm install --save-dev @babel/plugin-proposal-explicit-resource-management
Enter fullscreen mode Exit fullscreen mode

Add to your .babelrc:

{
  "plugins": ["@babel/plugin-proposal-explicit-resource-management"]
}
Enter fullscreen mode Exit fullscreen mode

With Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-explicit-resource-management
Enter fullscreen mode Exit fullscreen mode
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          '@babel/plugin-proposal-explicit-resource-management'
        ]
      }
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

With Next.js:

# Install the resource management plugin
npm install --save-dev @babel/plugin-proposal-explicit-resource-management
Enter fullscreen mode Exit fullscreen mode
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    "@babel/plugin-proposal-explicit-resource-management"
  ]
}
Enter fullscreen mode Exit fullscreen mode

With tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-explicit-resource-management
Enter fullscreen mode Exit fullscreen mode
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              '@babel/plugin-proposal-explicit-resource-management'
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Making Objects Disposable:

To make your objects work with using, implement the Symbol.dispose method:

class MyResource {
  [Symbol.dispose]() {
    // Cleanup code here
  }
}
Enter fullscreen mode Exit fullscreen mode

For async resources, implement Symbol.asyncDispose:

class MyAsyncResource {
  async [Symbol.asyncDispose]() {
    // Async cleanup code here
  }
}
Enter fullscreen mode Exit fullscreen mode

Polyfill for Symbols:

// polyfill.js
if (!Symbol.dispose) {
  Symbol.dispose = Symbol("Symbol.dispose");
}

if (!Symbol.asyncDispose) {
  Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
}
Enter fullscreen mode Exit fullscreen mode

Decorators Add Functionality Without Changing Core Logic

JavaScript decorators provide a clean way to modify classes and methods with additional functionality. If you've used TypeScript or frameworks like Angular, you're already familiar with the concept. Now decorators are coming to vanilla JavaScript.

Decorators solve a fundamental problem: how to add cross-cutting concerns like logging, validation, or performance monitoring without cluttering your business logic.

Here's a practical example:

class UserController {
  @authenticate
  @rateLimit(100)
  @validate(userSchema)
  @logAccess
  async updateUserProfile(userId, profileData) {
    // The method is automatically wrapped with:
    // 1. Authentication check
    // 2. Rate limiting (100 requests per hour)
    // 3. Input validation against userSchema
    // 4. Access logging

    const user = await this.userRepository.findById(userId);
    Object.assign(user, profileData);
    return this.userRepository.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Without decorators, you'd need to manually wrap each method, resulting in code that's harder to read and maintain:

class UserController {
  async updateUserProfile(userId, profileData) {
    // Authentication check
    if (!isAuthenticated()) {
      throw new Error('Unauthorized');
    }

    // Rate limiting
    if (isRateLimited(this.constructor.name, 'updateUserProfile', 100)) {
      throw new Error('Rate limit exceeded');
    }

    // Input validation
    const validationResult = validateWithSchema(userSchema, profileData);
    if (!validationResult.valid) {
      throw new Error(`Invalid data: ${validationResult.errors.join(', ')}`);
    }

    // Logging
    logAccess(this.constructor.name, 'updateUserProfile', userId);

    // Actual business logic
    const user = await this.userRepository.findById(userId);
    Object.assign(user, profileData);
    return this.userRepository.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's a real-world example of using decorators for performance monitoring:

class DataProcessor {
  @measure
  @cache
  processLargeDataset(data) {
    // Complex and time-consuming operation
    return data.map(item => /* complex transformation */)
               .filter(item => /* complex filtering */)
               .reduce((acc, item) => /* complex aggregation */);
  }
}

// Performance measurement decorator
function measure(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${name} took ${end - start}ms to execute`);
    return result;
  };
  return descriptor;
}

// Caching decorator
function cache(target, name, descriptor) {
  const original = descriptor.value;
  const cacheStore = new Map();

  descriptor.value = function(...args) {
    const key = JSON.stringify(args);
    if (cacheStore.has(key)) {
      console.log(`Cache hit for ${name}`);
      return cacheStore.get(key);
    }

    console.log(`Cache miss for ${name}`);
    const result = original.apply(this, args);
    cacheStore.set(key, result);
    return result;
  };

  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

Decorators are particularly valuable for API development. Here's an example of using decorators to implement a RESTful API with proper error handling:

class ProductAPI {
  @route('GET', '/products')
  @paginate
  @handleErrors
  async getAllProducts(req) {
    return this.productRepository.findAll();
  }

  @route('GET', '/products/:id')
  @handleErrors
  async getProductById(req) {
    const product = await this.productRepository.findById(req.params.id);
    if (!product) {
      throw new NotFoundError(`Product with ID ${req.params.id} not found`);
    }
    return product;
  }

  @route('POST', '/products')
  @validate(productSchema)
  @handleErrors
  async createProduct(req) {
    return this.productRepository.create(req.body);
  }
}

// Route decorator
function route(method, path) {
  return function(target, name, descriptor) {
    if (!target.constructor._routes) {
      target.constructor._routes = [];
    }

    target.constructor._routes.push({
      method,
      path,
      handler: descriptor.value,
      name
    });

    return descriptor;
  };
}

// Error handling decorator
function handleErrors(target, name, descriptor) {
  const original = descriptor.value;

  descriptor.value = async function(req, res) {
    try {
      const result = await original.call(this, req);
      return res.json(result);
    } catch (error) {
      if (error instanceof NotFoundError) {
        return res.status(404).json({ error: error.message });
      }
      if (error instanceof ValidationError) {
        return res.status(400).json({ error: error.message });
      }
      console.error(`Error in ${name}:`, error);
      return res.status(500).json({ error: 'Internal server error' });
    }
  };

  return descriptor;
}
Enter fullscreen mode Exit fullscreen mode

How to Use Decorators Today

Decorators are at Stage 3 in the TC39 process as of May 2025, with implementations in progress in major browsers. Here's how to use them today:

With Babel (Generic Setup):

# Install the decorators plugin
npm install --save-dev @babel/plugin-proposal-decorators
Enter fullscreen mode Exit fullscreen mode

Add to your .babelrc:

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

With Vite:

# Install dependencies
npm install --save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-decorators
Enter fullscreen mode Exit fullscreen mode
// vite.config.js
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';

export default defineConfig({
  plugins: [
    babel({
      babelConfig: {
        plugins: [
          ['@babel/plugin-proposal-decorators', { version: '2023-05' }]
        ]
      }
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

With Next.js:

# Install the decorators plugin
npm install --save-dev @babel/plugin-proposal-decorators
Enter fullscreen mode Exit fullscreen mode
// .babelrc
{
  "presets": ["next/babel"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-05" }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

With TypeScript:

Enable decorators in tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true
  }
}
Enter fullscreen mode Exit fullscreen mode

With tsup:

# Install dependencies
npm install --save-dev tsup @babel/core @babel/plugin-proposal-decorators
Enter fullscreen mode Exit fullscreen mode
// tsup.config.ts
import { defineConfig } from 'tsup';
import * as babel from '@babel/core';
import fs from 'fs';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  esbuildPlugins: [
    {
      name: 'babel',
      setup(build) {
        build.onLoad({ filter: /\.(jsx?|tsx?)$/ }, async (args) => {
          const source = await fs.promises.readFile(args.path, 'utf8');
          const result = await babel.transformAsync(source, {
            filename: args.path,
            presets: [
              ['@babel/preset-env', { targets: 'defaults' }],
              '@babel/preset-typescript'
            ],
            plugins: [
              ['@babel/plugin-proposal-decorators', { version: '2023-05' }]
            ]
          });

          return {
            contents: result?.code || '',
            loader: args.path.endsWith('x') ? 'jsx' : 'js'
          };
        });
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Important Note:

The decorator proposal has gone through several iterations. Make sure you're using the latest syntax (2023-05 version) as older versions are incompatible. The current specification defines three capabilities for decorators:

  • They can replace the decorated value with a matching value
  • They can provide access to the decorated value
  • They can initialize the decorated value

Testing Your Configuration

After setting up your configuration, create a simple test file to verify that the features are working:

// test-features.js

// Test pipeline operator
const double = x => x * 2;
const add = x => x + 1;
const square = x => x * x;

const result = 5
  |> double
  |> add
  |> square;

console.log("Pipeline result:", result); // Should be 121

// Test pattern matching
function processValue(value) {
  return match (value) {
    when String(s) => `String: ${s}`,
    when Number(n) => `Number: ${n}`,
    when { type, data } => `Object with type ${type}`,
    when _ => 'Unknown'
  };
}

console.log("Pattern matching:", processValue("test"), processValue(42), processValue({ type: "user", data: {} }));

// Test Temporal API
import { Temporal } from '@js-temporal/polyfill';
const now = Temporal.Now.plainDateTimeISO();
console.log("Current time:", now.toString());

// Test resource management
class Resource {
  constructor(name) {
    this.name = name;
    console.log(`Resource ${name} created`);
  }

  [Symbol.dispose]() {
    console.log(`Resource ${this.name} disposed`);
  }
}

{
  using resource = new Resource("test");
  console.log("Using resource");
} // Resource should be disposed here

// Test decorators
function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with ${JSON.stringify(args)}`);
    return original.apply(this, args);
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
console.log("Decorator result:", calc.add(2, 3));
Enter fullscreen mode Exit fullscreen mode

Conclusion

These upcoming JavaScript features aren't just syntactic sugar - they fundamentally change how we write code. At Lingo.dev, we've already seen how they can transform complex, error-prone code into clean, maintainable solutions.

The best part is you don't have to wait. With the right tools, you can start using these features today. Each one solves real problems that developers face daily, from managing resources to handling dates correctly.

What upcoming JavaScript feature are you most excited about? Have you found other ways to solve these problems in your codebase?

Share your thoughts!


Useful links:

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (26)

Collapse
 
steven_dix_755c2b861b6d92 profile image
Steven Dix

I don't think i agree with the pipeline operator being as useful however, or maybe i just have an issue with the example as it seems very apples to oranges. It's displayed that it's an alternative to a set of nested functions but tbh if i was creating functions to be chained ( like how Arrays have ). I would just do this:

export class User {
  private user: User;

  constructor(user: User) {
    this.user = user;
  }

  static enrichUserProfile() {
    /* do something and amend this.user */
    return this;
  }

  static normalizeData() {
    /* do something and amend this.user */
    return this;
  }

  static validateUser() {
    /* do something and amend this.user */
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

and i would then argue that just having User(ourUser).enrichUserProfile().normalizeData().validateUser(). saveToDatabase()

Is nicer than a |> between each method, and needing to polyfill for earlier versions.

Collapse
 
framemuse profile image
Valery Zinchenko

In case of a class build composition, you can't use other methods in the build chain, but with pipeline - you can.

Collapse
 
vrcprl profile image
Veronica Prilutskaya 🌱

Another great article, Max! Thanks for sharing!

Love the new pipeline operator (|>) - the code looks so clean!

Collapse
 
maxprilutskiy profile image
Max Prilutskiy

we're moving parts of the codebase to use it now. it's great!

Collapse
 
ryanguitar profile image
Ryan Els

Cool 😎 👌

Collapse
 
danibrear profile image
Dani Brear

Been writing elixir for 6 months and these are all things I was thinking “wow, this would be cool if I could do in JS”. So cool to see the proposals coming down the pipe (pun not intended but appreciated)

Collapse
 
dantman profile image
Daniel Friesen

I live that Temporal is finally becoming a thing. Though it doesn't feel right to call the article "upcoming" when it's talking about some proposals that are in stage 2 and 1 and were in that stage last time I checked ages ago. I'd really like to see some progress to be excited about, like Temporal, them an article would be exciting.

Collapse
 
dotallio profile image
Dotallio

This was packed with so many practical examples, love it! For me, the pipeline operator is a total game changer for making workflows readable, but pattern matching is right up there too. Have you run into any gotchas using the Babel plugin in bigger projects?

Collapse
 
jswhisperer profile image
Greg, The JavaScript Whisperer

Really nice article! I learnt some new tricks.
to me I think the gold star is Temporal API

I'm not sold on the pipeline operator, I'm on the fence about it, I kinda like the indentation of nested functions to visualise whats going on; and I think often it might be a code smell hint to refactor to async await promises if your code becomes nested like that. 🤷‍♂️

Collapse
 
thetos_ profile image
Thetos

I'm pretty sure the pipeline operator proposal has moved to a slightly different syntax with a placeholder symbol (like Hack) instead of these needing unary functions (like F#), so that it could play better with async/await among other things

Collapse
 
nevodavid profile image
Nevo David

Pretty cool seeing JavaScript finally fix a lot of old headaches - kinda pumped to mess with some of this now tbh.

Collapse
 
jkalandarov profile image
Jasurbek Kalandarov

When all languages try to transition to js syntax, js wants to adopt PHP syntax??

Collapse
 
alkawero_ef342b4b2a1d5995 profile image
alkawero

do you mean pipeline operator? it is not from PHP, F# was the first use it

Collapse
 
narehate78 profile image
Nare Hate

I really like the pipeline operator!
Regarding Date, I though Moment.js was deprecated and not maintained anymore, am I wrong since you're talking about it? Luxon is my way to go now.

Collapse
 
maxprilutskiy profile image
Max Prilutskiy

Luxon is nice! Moment.js is still in lots of codebases though.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay