Правила ниже предотвращают больше всего ошибок в реальном коде на 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.