Перейти к содержанию

Вывод типов и аннотации

В первом уроке мы явно записали тип суммы: total: number. Если убрать эту аннотацию, TypeScript всё равно может определить тип результата по исходным значениям и операции сложения. Это называется выводом типов. Он уменьшает количество повторений, но не заменяет решение автора о том, какие значения допустимы на границе функции или модуля.

Рассмотрим тот же каталог четырёх курсов. Данные и настройки lesson-01 сохраняются; меняется только src/app.ts. Наша задача — отличить удобный вывод типа локального вычисления от полезной аннотации публичного поведения. JavaScript-преобразования мы здесь не повторяем: интересует то, какую информацию инструмент получает из исходного текста и какую информацию должны добавить вы.

Что видно из присваивания

Когда переменная сразу получает сумму числовых полей, инструмент видит достаточно информации. Выражение reduce начинается с нуля, на каждом шаге добавляет course.lessons и возвращает число. Повторять number после имени локальной переменной в этом месте не обязательно. Важнее, чтобы исходные поля не превратились в неоднозначные значения.

Замените весь src/app.ts следующим файлом. src/data.ts, HTML и конфигурация остаются прежними:

import { courses } from './data.js';

function formatCount(lessons: number): string {
  return `Уроков: ${lessons}`;
}

const total = courses.reduce((sum, course) => sum + course.lessons, 0);
let requestedTopic: string = 'frontend';
requestedTopic = 'publishing';
console.log(formatCount(total));
console.log(`Выбрана тема: ${requestedTopic}`);

Ожидаются две строки:

Уроков: 60
Выбрана тема: publishing

У total тип выведен из вычисления. У параметра lessons и результата formatCount типы записаны явно. Функция получает число и создаёт строку; это удобно понимать, не заглядывая в реализацию. Возвращаемая строка не становится числом только потому, что содержит цифры. Подпись и количество — разные значения с разными разрешёнными операциями.

Для requestedTopic выбрана аннотация string. Переменная может позже получить другую строку, поэтому присваивание publishing соответствует контракту. Пока мы разрешаем вообще любую строку; конечный набор тем выразим отдельным объединением в четвёртом уроке. Не стоит заранее подменять эту учебную границу утверждением, что строковый тип уже проверяет существование рубрики.

Сделайте мысленное изменение: вместо числа передайте formatCount('60'). JavaScript способен хранить такую строку, но объявленный параметр требует число. Инструмент должен указать несовпадение до выполнения. Исправлением будет либо числовой аргумент, либо другая осмысленная сигнатура функции. Запись as number не преобразует строку и потому не решает задачу.

Тип значения и намерение автора

Вывод опирается на то, что реально записано. Если написать const heading = 'Каталог', неизменяемая переменная может сохранять более точную информацию о строковом литерале. Для let heading = 'Каталог' важно оставить возможность присвоить другую строку. Разницу видно в подсказке редактора, но не в выводе браузера: обе начальные строки будут одинаковыми.

У объектов ситуация требует отдельного внимания. const course = { lessons: 20 } запрещает заменить саму переменную другим объектом, но не запрещает изменить поле course.lessons. Поэтому обычное изменяемое поле не обязано сохранять узкий тип ровно одного числа. Представление о том, что const автоматически делает весь граф данных неизменным, привело бы к неправильным ожиданиям от каталога.

Удобно задавать себе два вопроса. Во-первых, можно ли получить нужный тип, прочитав правую сторону выражения? Во-вторых, должна ли левая сторона принять больше вариантов, чем присутствует в начальном значении? Для локальной суммы ответ на первый вопрос положительный. Для будущего фильтра, начинающегося с одной темы и меняющегося на другие, второй вопрос тоже важен.

Не стремитесь поставить аннотацию после каждого имени. Большая часть тела функции уже окружена известным контекстом. Например, callback reduce получает курс из импортированного массива, поэтому его параметр не является произвольным неизвестным объектом. Ручное повторение формы курса во всех callback быстро разойдётся при добавлении нового поля.

Контекст у callback и пустые данные

Вывод работает не только справа налево. В вызове метода массива инструмент знает тип элемента и использует его для параметра callback. В нашем вычислении course.lessons имеет смысл именно потому, что course происходит из courses. Если вынести callback в отдельную функцию без аннотаций и без контекста назначения, такой источник информации может исчезнуть.

Рассмотрим фрагмент для чтения, который можно временно добавить после импортов:

const counts: number[] = [];
counts.push(20);
counts.push(16);
const sum = counts.reduce((total, value) => total + value, 0);
console.log(sum);

Для пустого массива автор явно задаёт, что будущие элементы являются числами. Само отсутствие элементов не рассказывает, какой контракт планировался. После двух добавлений ожидается 36. Это отдельный небольшой пример для понимания вывода, он не заменяет четыре исходные записи каталога.

Сравните этот случай с массивом объектов, уже содержащим данные. Там тип элемента можно получить из записей. Но если в одном объекте число уроков ошибочно станет строкой, вывод может описать неоднородность вместо желаемого ограничения. В следующем уроке интерфейс позволит выразить желаемую форму до анализа частных объектов.

Другой характерный случай — переменная для ещё не полученного курса. Если автор записывает только undefined, из этого нельзя вывести намерение позже хранить объект курса. Нужен более широкий договор, который включает отсутствие и наличие. Мы подробно введём его после определения Course, чтобы не скрывать от читателя неизвестное имя типа.

Аннотация не является исправлением данных

Числовое поле иногда приходит из HTML-формы строкой. Желание получить number правильно, но одна аннотация не выполняет преобразование. Нужны отдельное чтение строки, преобразование и проверка результата. Пока источник каталога — локальный data.ts; смешивать эти две ситуации не следует. Разделение пригодится при будущей работе с формой и API.

Сходным образом тип возвращаемого значения не создаёт недостающий return. Если функция обещает строку, а одна ветка ничего не возвращает, причина находится в реализации. Правильное решение — явно определить результат ветки, а не расширять контракт случайно ради исчезновения сообщения инструмента. Контракт должен описывать задуманный результат операции.

Когда подпись защищает от незаметного изменения

Представьте, что автор меняет формат сводки: вместо строки функция начинает возвращать объект с числом и подписью. Для нового интерфейса это может быть разумно. Но прежняя явно записанная сигнатура : string обнаружит, что наружное обещание изменилось. Тогда нужно сознательно согласовать вызывающие места, а не рассчитывать, что изменение останется незаметным.

В отличие от этого прозрачная локальная переменная total не обязана сохранять отдельную аннотацию ради каждой операции. Её частный тип виден из вычисления. Если формула начнёт формировать текст, изменение проявится там, где результат используется как число. Чем ближе значение к границе модуля или публичному параметру, тем полезнее отдельное заявление намерения.

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

Для самостоятельного чтения замените requestedTopic на ещё одну произвольную строку. Во втором уроке это допустимо: выбран договор string. Из этого не следует, что каталог способен показать такую тему. Мы пока показываем саму строку, а не используем её в конечном наборе фильтра. Такое различие помогает читать пример по фактическому результату и не приписывать выводу типов правила, которые автор ещё не выразил.

Связь аннотаций и вывода показана в Everyday Types. Для нашей модели полезно оставить вывод у прозрачных локальных вычислений, а аннотации использовать там, где необходимо выразить намерение: параметры функции, тип будущих элементов, состояние с несколькими допустимыми вариантами.

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

Оглавление курса.