Система типов — Примеры

Пример 1 — Опечатка, которую нашёл бы только рантайм

На границе сумма корзины типизирована как any, так что свойство с опечаткой даёт NaN и уходит в продакшен. Опишите форму, которая функции действительно нужна, и скажите, что any делает со всеми производными значениями.

function totalCents(items: any) {
  return items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0);
}

totalCents([{ priceInCents: 300, quantity: 2 }]);   // NaN, и об ошибке нигде не сообщается

Решение

type CartItem = { priceCents: number; quantity: number };

function totalCents(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0);
}
totalCents([{ priceInCents: 300, quantity: 2 }]);
             ~~~~~~~~~~~~
error TS2353: Object literal may only specify known properties,
and 'priceInCents' does not exist in type 'CartItem'.

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

Пример 2 — null, доходящий до отрисовки

В записи пользователя удаление отмечено отметкой времени, которая допускает null, а функция подписи читает её без проверки. Скажите, что здесь меняет strict, и обработайте отсутствие.

type User = { id: string; name: string; deletedAt: string | null };

function label(user: User) {
  return user.name.toUpperCase() + " " + user.deletedAt.slice(0, 10);
}

Решение

function label(user: User): string {
  if (user.deletedAt === null) return user.name.toUpperCase();
  return user.name.toUpperCase() + " " + user.deletedAt.slice(0, 10);
}
user.deletedAt.slice(0, 10)
     ~~~~~~~~~~
error TS18047: 'user.deletedAt' is possibly 'null'.

Со включённым strict null — обычный тип, а не что-то, прячущееся за каждым значением, поэтому аннотация string | null становится утверждением, которое компилятор проверяет, а не комментарием. Это превращает «cannot read properties of null» из падения в продакшене в ошибку на строке, которая его вызвала, и стоит одного раннего возврата; явное моделирование отсутствия — самая ценная часть строгого семейства, поэтому это и есть параметр, который стоит включать первым: аннотация и проверка — одно и то же решение, записанное один раз.