Границы и strict
Вопрос
Что включает strict, и как тип не протекает через JSON, localStorage и ответ fetch?
Короткий ответ
strict в tsconfig включает пачку флагов. На собеседовании важны три.
strictNullChecks:nullиundefinedне входят в тип сами. Полеuser: User | nullнельзя читать, пока нет проверки.noImplicitAny: параметр без типа не становится тихимany.strictFunctionTypes: колбэк нельзя подсунуть с более широким аргументом, чем ждёт вызывающий.
Компилятор стирает типы. В JS их нет, поэтому данные снаружи всегда недоверенные. response.json() по типам DOM это any или unknown в зависимости от lib. Дальше нужен разбор: zod, ручной type guard или хотя бы проверка дискриминанта. as User на сыром JSON проверкой не является.
import type и import { type User } попадают только в типы и вырезаются из бандла. Обычный импорт класса или enum остаётся в JS.
Разбор
Опциональное поле name?: string — это string | undefined, и ключ можно не передавать. Поле name: string | undefined передать нужно, даже если значение undefined. Для пропсов компонента это разный контракт.
Non-null assertion input!.value говорит «здесь точно не null». Его ставят после проверки, которую компилятор не видит, например сразу после if (!input) return в другом замыкании он уже не сужает. Чаще безопаснее Optional chaining: input?.focus().
Модуль .d.ts описывает форму без реализации: типы глобала, window, чужой пакет без своих типов. Ошибка в таком файле не ловится тестом, пока код не дойдёт до этого вызова.
На что смотрят на senior
strict выключенный в старом проекте не чинят одним коммитом. Имеет смысл включать по папкам или править границу: публичные функции модуля получают типы, внутри на время остаётся долг.
Дженерик на границе API не заменяет схему. function parse<T>(raw: unknown): T врёт: вызывающий выбирает T, а функция ничего не проверила. Честная сигнатура возвращает конкретный тип или unknown и сужает внутри.
Типичные ошибки
- Включить
strictNullChecksв голове, а в проекте его нет, и объяснять поведение не того конфига. - Писать
as UserнаlocalStorage.getItem. - Импортировать тип значением и тащить в клиентский бандл модуль, который нужен только компилятору.
- Снимать
nullчерез!в каждой строке вместо одной проверки на входе.
Задача
Функция объявлена так. Почему вызов parse<User>(localStorage.getItem('user')) не делает значение пользователем?
function parse<T>(raw: string | null): T {
return JSON.parse(raw ?? '{}') as T
}Показать ответ
Дженерик выбирает вызывающий, тело ничего не проверяет. as T только заставляет компилятор замолчать. В рантайме в хранилище может лежать null, мусор или другой объект.
Честная граница возвращает unknown или конкретный тип после проверки полей. localStorage.getItem сам по себе всегда string | null, не User.