Привет! Представляю вашему вниманию перевод статьи Matt Pocock.
Источник

TypeScript 5.2 представит новое ключевое слово - using, которое можно использовать, чтобы избавиться от чего угодно с помощью функции Symbol.dispose, при покидании области видимости.

{
  const getResource = () => {
    return {
      [Symbol.dispose]: () => {
        console.log('Hooray!')
      }
    }
  }
  using resource = getResource();
} // 'Hooray!' logged to console

Обосновано это предложениями TC39, достигшими недавно Фазы 3, что демонстрирует скорое появление в JavaScript.

using будет очень полезным для управления ресурсами, такими как ссылки на файлы, соединения с базами данных и т.п.

Symbol.dispose

Symbol.dispose - новый глобальный символ в JavaScript. Всё что угодно с функцией, присвоеннойSymbol.dispose будет рассматриваться как 'ресурс' - "объект с особым жизненным циклом" - и может быть использовано со словом using.

const resource = {
  [Symbol.dispose]: () => {
    console.log("Hooray!");
  },
};

await using

Вы также можете использовать Symbol.asyncDispose и await using чтобы управлять ресурсами, которые должны быть распределены асинхронно.

const getResource = () => ({
  [Symbol.asyncDispose]: async () => {
    await someAsyncFunc();
  },
});
{
  await using resource = getResource();
}

Будет ожидать функцию Symbol.asyncDispose для продолжения.

Это будет полезно для ресурсов типа коннекторов БД, когда необходимо убедиться, что соединение закрыто, прежде чем продолжать.

Случаи использования

Указатели на файлы

Доступ к файловой системе с помощью хэндлеров на ноде может быть намного проще с using.

До:

import { open } from "node:fs/promises";
let filehandle;
try {
  filehandle = await open("thefile.txt", "r");
} finally {
  await filehandle?.close();
}

После:

import { open } from "node:fs/promises";
const getFileHandle = async (path: string) => {
  const filehandle = await open(path, "r");
  return {
    filehandle,
    [Symbol.asyncDispose]: async () => {
      await filehandle.close();
    },
  };
};
{
  await using file = getFileHandle("thefile.txt");
  // Do stuff with file.filehandle
} // Automatically disposed!

Соединения с БД

До:

const connection = await getDb();
try {
  // Do stuff with connection
} finally {
  await connection.close();
}

После:

const getConnection = async () => {
  const connection = await getDb();
  return {
    connection,
    [Symbol.asyncDispose]: async () => {
      await connection.close();
    },
  };
};
{
  await using { connection } = getConnection();
  // Do stuff with connection
} // Automatically closed!

Спасибо за внимание! Больше интересного по фронтенду - тут.

Комментарии (9)


  1. iliazeus
    03.07.2023 11:45
    +8

    Если сходить не в какую-то статью, а в блог самого TypeScript, то можно найти там вторую - и очень важную часть нововведения: DisposableStack и AsyncDisposableStack. С их помощью можно подчищать ресурсы, у которых ещё нет своего метода @@dispose/@@asyncDispose.

    function doSomeWork() {
        const path = ".some_temp_file";
        const file = fs.openSync(path, "w+");
    
        using cleanup = new DisposableStack();
        cleanup.defer(() => {
            fs.closeSync(file);
            fs.unlinkSync(path);
        });
    
        // use file...
    
        if (someCondition()) {
            // do some more work...
            return;
        }
    
        // ...
    }
    


  1. Aceki
    03.07.2023 11:45
    +1

    Похоже на деструкторы.


    1. iliazeus
      03.07.2023 11:45
      +7

      Точнее будет сказать, RAII. Реализация скорее похожа не на деструкторы C++, а на with в Python и using в C#.


      1. nin-jin
        03.07.2023 11:45
        +2

        Ну а в D это scope-guards и scoped-template.


  1. iliazeus
    03.07.2023 11:45
    +8

    Если сходить не в какую-то статью, а в блог самого TypeScript, то можно найти там вторую - и очень важную часть нововведения: DisposableStack и AsyncDisposableStack. С их помощью можно подчищать ресурсы, у которых ещё нет своего метода @@dispose/@@asyncDispose.

    function doSomeWork() {
        const path = ".some_temp_file";
        const file = fs.openSync(path, "w+");
    
        using cleanup = new DisposableStack();
        cleanup.defer(() => {
            fs.closeSync(file);
            fs.unlinkSync(path);
        });
    
        // use file...
    
        if (someCondition()) {
            // do some more work...
            return;
        }
    
        // ...
    }
    


    1. iliazeus
      03.07.2023 11:45
      +2

      Плюс, там же написана ещё одна важная вещь: TypeScript не будет автоматически полифиллить сами Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack. Полифиллы нужно подключать самому. Если стеки не нужны, а нужен только using, можно сделать просто:

      Symbol.dispose ??= new Symbol("Symbol.dispose");
      Symbol.asyncDispose ??= new Symbol("Symbol.asyncDispose");
      

      И работает это все пока только с таргетом es2022 или ниже, и esnext или esnext.disposable в lib.


    1. yet_it Автор
      03.07.2023 11:45

      Если сходить не в какую-то статью, а в блог самого TypeScript

      Ну кстати я сам удивился - статья от 16 июня, блог разрабов от 30 июня
      Есть идеи, почему так?)


      1. iliazeus
        03.07.2023 11:45
        +1

        Возможно, автор следил за разработкой фичи в ишью на гитхабе, и писал ее, ориентируясь на информацию оттуда.


  1. aamonster
    03.07.2023 11:45

    Т.е. с поведением в точности как деструкторы C++ объектов на стеке, я правильно понял? А по реализации это синтаксический сахар над try-finally?


    1. iliazeus
      03.07.2023 11:45
      +3

      Грубо говоря, да - уничтожаются тоже в обратном порядке при выходе из скоупа, включая выход по исключению. Вот здесь есть подробнее о семантике.

      Главное отличие, кажется, в том, что происходит, если dispose выбросил исключение - здесь вызов деструкторов продолжается, а все выброшенные исключения комбинируются в одно и выбрасываются после.