Menu

Лучшие практики TypeScript: 8 правил с примерами до и после

Восемь привычек в TypeScript, которые предотвращают реальные ошибки: не выключать strict, использовать unknown вместо any, доверять выводу типов, предпочитать объединения enum, проверять конфигурацию через satisfies, моделировать состояние размеченными объединениями, избегать ! и as и делать данные readonly. К каждой прилагается запускаемый пример до и после.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Правила ниже предотвращают больше всего ошибок в реальном коде на TypeScript. В каждом сначала показан распространённый вариант, а затем лучший, в коде, который можно запустить. Важнее всего первое правило: перестаньте использовать any.

Используйте unknown вместо any

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

Границы это места, где типы перестают быть гарантированными: JSON.parse, ответы fetch, localStorage, ввод из форм, переменные окружения и сообщения от других процессов. Проверяйте данные там защитником типа или библиотекой схем, и остальной код сможет доверять своим типам.

Не выключайте strict

В TypeScript 7 strict включён по умолчанию; не выключайте его. Добавьте проверки, которые он не включает и которые ловят больше всего ошибок:

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess заставляет arr[i] и record[key] включать undefined, а именно это они и возвращают для отсутствующего индекса. noImplicitOverride требует, чтобы метод подкласса был помечен override, а noFallthroughCasesInSwitch отвергает case, который проваливается в следующий.

Доверяйте выводу типов

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

Без комментария @ts-expect-error вызов setStatus(annotated) даёт ошибку TS2345. Выведенная const сохраняет литеральный тип "active", поэтому принимается. Прежде чем добавлять тип, наведите курсор на переменную в редакторе и посмотрите, что было выведено.

Предпочитайте объединения типов перечислениям

Объединение строковых литералов даёт автодополнение и проверку полноты без сгенерированного кода. Если список значений нужен ещё и во время выполнения, выведите тип из массива as const:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

enum Role { Admin, Viewer } компилируется в объект с обратным отображением, а параметр числового enum принимает любую переменную типа number, даже со значением, которого в enum нет. Кроме того, enum не работают при удалении типов в Node (TypeScript enum is not supported in strip-only mode). Плюсы и минусы сравниваются на странице enum.

Проверяйте объекты конфигурации через satisfies

Аннотация объекта широким типом вроде Record<string, Route> проверяет значения, но забывает ключи. satisfies проверяет то же самое и сохраняет точный тип:

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

Моделируйте состояние размеченными объединениями

Один объект с опциональными полями допускает невозможные состояния: loading: true вместе с error или отсутствие data после успеха. Объединение объектов с общей меткой допускает только реальные состояния, и каждая ветка видит только свои поля:

Строка с never это проверка полноты. Если кто-то добавит состояние и забудет его обработать, сборка сломается на этой строке:

Ошибка: index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Она называет необработанный случай. Добавьте case "cancelled":, и код скомпилируется.

Избегайте ! и as

Non-null утверждение x! и утверждение типа x as T велят компилятору прекратить проверку. Ни то, ни другое не меняет значение во время выполнения, поэтому ошибочное утверждение позже превращается в падение, далеко от своей причины:

Заменяйте ! на ?., ??, ранний return или брошенную ошибку с полезным сообщением. Заменяйте as на защитник типа, который действительно проверяет значение. Единственное всегда безопасное утверждение это as const, потому что оно только сужает тип и делает его доступным лишь для чтения. Хорошая настройка линтера (правила no-non-null-assertion и no-explicit-any из typescript-eslint) отмечает остальное.

Делайте данные readonly

Помечайте свойства и массивы readonly, когда код не должен их менять. Тогда компилятор отвергает push, sort и присвоения, а функции возвращают новые значения вместо изменения входных:

readonly проверяется только во время компиляции и только на один уровень вглубь: во время выполнения объект не замораживается. Но этого достаточно, чтобы поймать случайное изменение общего состояния, из-за которого и возникает большинство таких ошибок.

Часто задаваемые вопросы

Стоит ли использовать any в TypeScript?

В прикладном коде почти никогда. any отключает проверку для значения и всего, что из него получено. Для значений, тип которых вы пока не знаете, используйте unknown и сужайте его проверками; any оставьте для редких лазеек с комментарием, объясняющим причину.

Нужно ли аннотировать каждую переменную в TypeScript?

Нет. Пусть TypeScript выводит типы локальных переменных и параметров колбэков. Аннотируйте параметры функций (их нельзя вывести) и возвращаемые типы экспортируемых функций, чтобы изменение внутри функции не могло незаметно поменять её публичный тип.

Enum в TypeScript это плохая практика?

Не ошибка, но многие команды их избегают. Enum генерирует код для времени выполнения, не работает при удалении типов в Node, а параметр числового enum принимает любую переменную типа number, какое бы значение в ней ни было. Объединение строковых литералов или массив as const с выведенным типом дают то же автодополнение и те же проверки без сгенерированного кода.

Когда использовать утверждения типа через as?

Только когда вы знаете то, чего не может знать компилятор, и лучше сразу после проверки, которая это доказывает. as не меняет значение во время выполнения, поэтому {} as User компилируется, а name у него потом нет. В большинстве случаев безопаснее защитник типа, который проверяет значение.

Какие настройки tsconfig лучше всего подходят для нового проекта TypeScript?

Оставьте strict включённым (по умолчанию в TypeScript 7) и добавьте noUncheckedIndexedAccess. Многие проекты включают также noImplicitOverride, noFallthroughCasesInSwitch и verbatimModuleSyntax. Конфиг, который создаёт tsc --init, задаёт среди прочего strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes и verbatimModuleSyntax.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ