Официальный анонс Dart 3.9 здесь. А вот два новых линта, которые там не упомянули.

switch_on_type

Есть такой странный способ проверить тип переменной — switch по runtimeType:

class A {}

void main() {
  switch (variable.runtimeType) {
    case A:
      print('A');
    default:
      print('Something else');
  }
}

Проблема в том, что обычно мы хотим, чтобы подклассы A тоже попадали в первый кейс. Иначе мы не сможем подменять объекты, и полиморфизм, как учили в школе, не будет работать. Это большая проблема, потому что:

  • Объекты могут создаваться в коде, который мы не контролируем, поэтому имплементации могут поменяться без нашего ведома.

  • Нельзя делать моки для тестов, потому что пакеты типа mocktail и mockito работают через подмену имплементации.

  • И даже если вы полностью контролируете код, ни к чему создавать такое место, про обновление которого всегда надо помнить, если добавите подкласс.

Чтобы люди так не делали, добавился линт. Он заставляет переписать такой switch через паттерны:

switch (variable) {
  case A():
    print('A');
  default:
    print('Something else');
}

Теперь, если сделать B extends A, он тоже будет покрываться паттерном A(), и все моки будут работать.

Ладно, а как отделить A от B extends A? Надо сначала отбросить всю ветку наследования, которая начинается с B:

switch (variable) {
  case B(): // Соответствует B и всем его подклассам.
    break;
  case A(): // Соответствует A и всем подклассам, кроме B и его подклассов.
    print('A');
  default:
    print('Нечто совершенно иное');
}

Безопасные типы — мой выбор!

unnecessary_unawaited

Когда вызываем функцию, которая возвращает Future, обычно мы хотим дождаться результата. Иначе выполнение продолжится раньше, чем случится нужный нам эффект:

Future<void> save() { /* ... */ }

void fn() {
  save();  // LINT: discarded_futures
  close(); // Э, мы ещё не сохранили!
}

Поэтому ещё в Dart 2.18 сделали линт discarded_futures, который это помечает.

Чтобы он не срабатывал, нужно или дождаться результата через await, или обернуть вызов в unawaited():

Future<void> save() { /* ... */ }

Future<void> fn() async {
  unawaited(save()); // Это глупо.
  await save();      // Так-то лучше.
  close();
}

Однако, есть асинхронные функции, результат которых:

  • Почти никогда не нужен, поэтому глупо утомлять разработчиков таким линтом.

  • Иногда всё‑таки нужен, поэтому они всё равно возвращают Future.

Обычно это логирование, освобождение ресурсов и тому подобное:

Future<LogMessage> log(String message) { ... }

void fn() {
  unawaited(log('Message')); // Результат логирования обычно неважен.
}

И вот, чтобы это упростить, в пакет meta добавили аннотацию @awaitNotRequired, чтобы делать функции, которые не триггерят линт, если их не ждать:

@awaitNotRequired
Future<LogMessage> log(String message) { ... }

void fn() {
  log('Message'); // Прекрасно!
}

А если всё‑таки хочется дождаться результата:

@awaitNotRequired
Future<LogMessage> log(String message) { ... }

Future<void> fn() async {
  await log('Message'); // Никаких проблем.
}

Но если вы уже расставили везде unawaited(), то он теперь лишний. И новый линт поможет вам найти такие места:

@awaitNotRequired
Future<LogMessage> log(String message) { ... }

void fn() {
  unawaited(log('Message')); // LINT: unnecessary_unawaited
}

Более старые линты

Если вы пропустили мои предыдущие статьи, то вот:

Как подключить новые линты

Прочитайте эту статью о том, как подключить эти правила вручную.

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

Не пропускайте мои статьи, добавляйтесь в Телеграм‑канал: ainkin_com
Русские переводы реже и с задержкой здесь: ainkin_com_ru

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


  1. lil_master
    01.11.2025 06:11

    Не могу поставить плюс, поэтому ловите виртуальный) Спасибо, отличный разбор!
    Вопросик:
    В статье упоминаются подклассы, но не затрагивается тема sealed-иерархий. Как, по-вашему, этот линт будет (или должен) взаимодействовать с exhaustive_cases? Кажется, что связка switch_on_type (как толчок к переходу на паттерны) и exhaustive_cases (для sealed) — это и есть та "золотая середина" для безопасной работы с типами, к которой нас подталкивает язык. Не упускаем ли мы здесь более крупную картину, фокусируясь только на runtimeType?

    Я бы посоветовал всем, кто будет рефакторить свой код под switch_on_type, не просто механически менять синтаксис. Стоит сразу оценить, не является ли вся эта иерархия классов-кандидатом на sealed-модификатор. Зачастую это решает проблему на более глубоком, архитектурном уровне, а не просто "лечит" линтер.