openlang

Аннотация или satisfies для объекта конфигурации

Для объектов конфигурации теперь в основном пишу {...} satisfies Config вместо const config: Config = {...}. С аннотацией config.mode читается как 'light' | 'dark', хотя написано 'dark'. С satisfies объект проверяется на соответствие Config, а переменная сохраняет литеральные типы, с которыми её написали. Поэтому config.mode остаётся 'dark', и сравнение с 'light' где-то дальше становится ошибкой типов, а не мёртвой веткой.

Опечатку в имени свойства литерала ловят оба варианта. В заметках к 4.9 это показано на ключе bleu, который проверяют против Record<Colors, string | RGB>.

Подвела меня таблица маршрутов, записанная как satisfies Record<string, Route>. Там опечатка в имени маршрута - не лишнее свойство. Разрешён любой строковый ключ, проверка проходит, и опечатка превращается в маршрут, на который никто не ссылается. У аннотации та же дыра. Просто с satisfies казалось, что проверок больше, чем было на самом деле.

Если набор ключей фиксирован, теперь выписываю объединение явно, Record<RouteName, Route>, и пусть объект падает, когда в нём заводится чужак.

Разговор продолжается 5

Следуя доводам
  1. Ссылка на ответ

    Ещё одно различие, на необязательных свойствах. С аннотацией пропущенный в литерале timeout?: number всё равно читается как number | undefined, и в каждом месте, где его читают, приходится решать, что значит undefined. С satisfies этого свойства в выведенном типе нет вовсе, и config.timeout падает с ошибкой TS2339, свойство не существует.

    Ответ жёстче и при этом другой. Добавьте timeout в литерал позже, и все отвергнутые чтения начнут компилироваться. Выбирать стоит по тому, должен ли вызывающий код обрабатывать отсутствие значения или вовсе его не видеть.

  2. Ссылка на ответ

    Для всего, что экспортируется, я бы пошёл в другую сторону. Объект с satisfies экспортирует и свой литеральный тип, так что каждый модуль, импортирующий config, теперь зависит от значений внутри. Поменяйте 'dark' на 'light', и сравнение через три файла перестанет компилироваться. Тот код опирался на значение, а не на тип, и никто этого не выбирал. Для конфигурации приложения я по-прежнему за аннотацию. Config говорит, на что коду можно опираться, не больше.

  3. Ссылка на ответ

    В корневом сообщении с литералами всё выглядит проще, чем есть. satisfies сохраняет не каждый написанный литерал.

    type Config = { mode: 'light' | 'dark'; port: number };
    
    const config = { mode: 'dark', port: 8080 } satisfies Config;
    
    const m: 'dark' = config.mode;  // ok
    const p: 8080 = config.port;    // error, port is number

    port расширяется до number, потому что Config даёт ему контекстный тип number. mode остаётся 'dark' только потому, что в Config он объявлен как объединение строковых литералов. Сохраняется более конкретный выведенный тип, а насколько он конкретен, зависит от Config. Опасение, что импортирующие модули начнут опираться на значения, касается mode и почти не касается port.

  4. Ссылка на ответ

    Итак, пользователь открывает настройки, выбирает тёмную тему, и этот выбор записывается в JSON-файл. При следующем запуске приложение читает этот файл обратно, а satisfies рядом с ним и не стоял (он смотрел только на литерал в исходнике). Литерал config говорит 'dark', а в сохранённом файле может оказаться 'Dark' или вообще ничего. Как обычно делают, держат объект с satisfies как значения по умолчанию и разбирают сохранённый файл в Config с проверкой во время выполнения? Или есть способ не дать им разойтись, который я упускаю?

  5. Ссылка на ответ

    Отвечаю на свой же вопрос, уже попробовав. Литерал с satisfies стал defaults, сохранённый файл проходит через маленький parseSettings(raw: unknown): Partial<Config>, а приложение работает с const config: Config = { ...defaults, ...saved }. Аннотация на объединённом значении заодно снимает опасение selim70, ведь импортирующие модули видят только Config.

    Подвёл парсер. Первая версия копировала все известные ключи и ставила undefined, если значение было мусорным. Спред явного undefined затирает значение по умолчанию, а типы промолчали (в том проекте не было exactOptionalPropertyTypes). В итоге пользователь, который вписал "Dark" руками, получал не тёмную и не светлую тему, а экран выбора темы, где ничего не отмечено. Теперь парсер просто выкидывает плохой ключ, а экран настроек показывает короткую заметку, что одно сохранённое значение проигнорировано.

Другие стороны того же вопроса

  1. Единицы измерения как дешёвые идентификаторы

    Для целочисленных идентификаторов я пробую единицы измерения вместо объединений с одним случаем, и хочется услышать второе мн…

    Ответы 1fsharp
  2. Хелпер, перенесённый из Java, в Groovy вызывает другую перегрузку

    Java-класс хелпера переехал в Groovy на стороне сборочного скрипта, и тот же вызов ушёл в другой метод. По порядку. В Java об…

    Ответы 3groovyjava
  3. Пустая ячейка равна и нулю, и пустой строке

    Достался макрос, который помечает строки заказов с нулевым количеством, и он пометил заодно все пустые строки. Проверка была…

    Ответы 7vba