Skip to content
14 / 37

Что такое using и Disposable в TypeScript 5.2?


Полный ответ

Проблема: ресурсы (файлы, соединения, транзакции) нужно закрывать вручную, и это легко забыть:

TypeScript
// ❌ Без using — нужен try/finally

function readConfig() {
  const file = openFile("config.json");
  try {
    return file.read();
  } finally {
    file.close(); // легко забыть
  }
}

С using очистка происходит автоматически:

TypeScript
// Объект должен реализовать Disposable

class FileHandle implements Disposable {
  private handle: number;

  constructor(path: string) {
    this.handle = openRawFile(path);
    console.log(`Opened: ${path}`);
  }

  read(): string {
    return readFromHandle(this.handle);
  }

  [Symbol.dispose](): void {
    closeRawFile(this.handle);
    console.log("File closed");
  }
}

function readConfig() {
  using file = new FileHandle("config.json");
  return file.read();
  // [Symbol.dispose]() вызывается автоматически при выходе из блока
}

Асинхронный вариант для сетевых ресурсов:

TypeScript
class DbConnection implements AsyncDisposable {
  private connection: RawConnection;

  private constructor(conn: RawConnection) {
    this.connection = conn;
  }

  static async connect(url: string): Promise<DbConnection> {
    const conn = await rawConnect(url);
    return new DbConnection(conn);
  }

  async query(sql: string): Promise<unknown[]> {
    return this.connection.execute(sql);
  }

  async [Symbol.asyncDispose](): Promise<void> {
    await this.connection.close();
    console.log("Connection closed");
  }
}

// await using — для AsyncDisposable
async function getUsers() {
  await using db = await DbConnection.connect("postgres:// localhost/app");
  return db.query("SELECT * FROM users"); // соединение закроется автоматически
}

Несколько ресурсов освобождаются в обратном порядке:

TypeScript
async function transfer() {
  await using source = await DbConnection.connect("postgres:// source");
  await using target = await DbConnection.connect("postgres:// target");

  const data = await source.query("SELECT * FROM accounts");
  // ... перенос данных ...

  // Порядок dispose: target первым, потом source (LIFO)
}

Фабричная функция, возвращающая Disposable:

TypeScript
function createTempDir(): Disposable & { path: string } {
  const path = mkdtempSync("/tmp/app-");
  console.log(`Created: ${path}`);

  return {
    path,
    [Symbol.dispose]() {
      rmSync(path, { recursive: true });
      console.log(`Removed: ${path}`);
    },
  };
}

function buildProject() {
  using tmp = createTempDir(); // используем tmp.path для временных файлов
  // при выходе из функции папка удалится
}

Реальные кейсы

Лок на ресурс:

TypeScript
class Mutex {
  private locked = false;
  private waiters: (() => void)[] = [];

  async acquire(): Promise<Disposable> {
    while (this.locked) {
      await new Promise<void>((resolve) => this.waiters.push(resolve));
    }
    this.locked = true;

    return {
      [Symbol.dispose]: () => {
        this.locked = false;
        this.waiters.shift()?.();
      },
    };
  }
}

const mutex = new Mutex();

async function criticalSection() {
  using lock = await mutex.acquire(); // безопасная работа с общим ресурсом
  // лок автоматически отпустится
}

Ошибка: забыть using и потерять автоочистку:

TypeScript
// ❌ Обычный const — dispose не вызовется
const file = new FileHandle("data.txt"); // file[Symbol.dispose]() не вызовется автоматически

// ✅ using — dispose вызовется при выходе из скоупа
using file2 = new FileHandle("data.txt");

Резюме

using и await using автоматически вызывают [Symbol.dispose]() или [Symbol.asyncDispose]() при выходе из блока. Это устраняет целый класс багов с утечкой ресурсов. Для работы нужен TypeScript 5.2+ и target ES2022 или полифилл для Symbol.dispose.

Что такое using и Disposable в TypeScript 5.2? | JScriptiser