Данная статья является первой в цикле статей о практическом применении языка TypeScript в React-приложениях. Амбициозная цель автора — в конечном счёте написать книгу, которая будет ближе к практике чем к теории. В этом смысле книга не будет разжёвывать документацию языка, а предложит рассмотрение подходов и примеров типизации отдельных аспектов React-приложений.

Это далеко идущие планы, а на текущий момент задача цикла статей — собрать фидбек о правильности авторских суждений, которые основаны на личном опыте разработки приложений на TypeScript и местами противоречат популярной литературе по языку TypeScript. Тем не менее доводы к этим суждениям должны заинтересовать сообщество фронтэнд-разработчиков.

О языке TypeScript

TypeScript расширяет возможности JavaScript-программиста, добавляя статичную типизацию в код. С началом его использования в арсенале разработчика появляется анализатор кода, который проверяет соответствие типов, подсвечивает подсказки и ошибки. Таким образом этот язык скорее помогает писать код правильно, чем добавляет новые возможности в JavaScript, как многие склонны думать.

Негативный опыт связанный с использованием языка TypeScript у ряда разработчиков связан с тем, что он во-первых подсвечивает слишком много непонятных ошибок. Создаётся впечатление, что это TypeScript создаёт ошибки, ведь на JavaScript таких проблем нет. Для исправления приходится гуглить решения и писать много дополнительного кода. Появляются сомнения в необходимости использования Typescript — он больше требует усилий на обслуживание, чем облегчает разработку.

Во-вторых, старожилы разработки, привыкшие писать функции с проверкой данных, умеют защищаться от ошибок с типами. Они следуют best practice в наименовании переменных и структурирования данных, поэтому имеют множество доводов не использовать TypeScript в проектах.

Например, следующий пример функции на JavaScript, включает проверку типа входных данных:

const getUrlById = (id) => {
  if (typeof id !== 'string' || typeof id !== 'number') {
      return
  }
  return 'http://my-url.ru/' + id
}

Код 1.1. Функция на JavaScript с проверкой типа аргумента

Тем не менее TypeScript основательно закрепился во многих российских веб-проектах. Проблема крупных приложений заключается не в большом объёме кода, а в сложности его поддержания, сложности чтения и удорожания расширения. TypeScript является одним из звеньев решения этих проблем.

Люди не могут предсказывать работу сложных компьютерных систем. Даже программисты. Особенно программисты. Наша возможность создавать огромные и сложные структуры ошибочно позволяет нам думать, что мы в состоянии постичь их до конца.

© Карлос Буэно

TypeScript на ряду с именами переменных документирует код. Самодокументирующийся код основан на грамотном именовании переменных. Этот подход упрощает понимание логики при чтении кода. При этом для прослеживания потока данных помогает TypeScript. Стоит чуточку увеличить вложенность кода, как в голове разработчика начинает твориться хаос. А для крупных проектов это неизбежно. Когда функция вызывает другую, а та третью, то переменные могут называть так, как это уместно в локальном пространстве каждой из них. И только через типизацию мы видим, как данные распределяются, преобразуются при передаче из кода в код. Наименование типов позволяет разработчику рисовать в голове структуру отдельного функционала.

Тот же смысл имеет использование TypeScript при разработке. Попробуйте при проектировании функционала думать не о том, что должно происходить в коде, а о том, как должны преобразовываться модели данных.

Любой программный код — это жонглирование данными.

Пример, демонстрирующий типизацию на разных уровнях потока данных :

// Внимание! Пример всего лишь демонстрирует описание структуры данных на разных слоях,
// но не показывает как лучше их описывать

type BackendUserData = {
  name: string | null;
  secondName: string | null;
  lastName: string | null;
  phone: number | null;
  passport: string | null;
}

type UserFormValues = {
  name: string;
  secondName: string;
  lastName: string;
}

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

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

Контекст типов и контекст значений

TypeScript содержит синтаксические конструкции похожие на JavaScript. Например, угловые скобки < > есть в JSX и есть в Generic'ах. Тоже самое с операторами typeof и тернарным оператором ? :. Каждый из них работает по-разному и это зависит от того, принадлежит ли он языку JavaScript или языку TypeScript.

Чтобы понимать, к какому языку относится оператор, конструкция или переменная нужно отличать контекст типов и контекст значений. Контекст в данном случае - это место в коде, которое может относиться или к типизации, или к логике.

Лучше это понять поможет инструкция присвоения:

const string: number = 1;

Код 1.2.Инициализация переменной с типизацией.

По началу может смутить странное имя у переменной string. Нарушение соглашения об именовании переменных сделано умышленно, чтобы показать что даже если переменную назвать именем типа, всё равно можно понять, что это имя переменной, а не тип, так как оно написано в контексте значений. Тип number описан в контексте типов.

В данном случае контекст типов располагается при инициализации переменной после двоеточия. Есть ряд правил, позволяющих анализатору TypeScript отличить контекст значений от контекста типа. Почти весь код, которым TypeScript расширяет код JavaScript, является контекстом типов, всё остальное — контекстом значений. Начинающему разработчику к этому нужно просто привыкнуть на практике.

Другой пример с контекстами типов и значений:

const fn = () => {

    type MyType = {
        one: number;
        two: number;
    }    

    interface MyInterface {
        a: string,
        b: string,
    }

    const Mytype: MyType = { one: 1, two: 2 }
    
    return Mytype;
}

Код. 1.3. Описание типов и интерфейсов внутри функции

В этом коде как и в предыдущем нарушены привычные соглашения. Снова умышленно. Во-первых, тип и интерфейс описаны внутри функции, во-вторых, имя типа и имя переменной совпадают. Возникает вопрос, что же вернёт функция: тип или значение?

Чтобы ответить на него определим, где здесь контекст типов, а где контекст значений. Всё, что написано внутри type и interface, относится к контексту типов. Всё остальное — контекст значений. Поэтому при вызове fn() функция вернёт значение:

const value = fn() // { one: 1, two: 2 }

Подобные эксперименты с написанием типов подводят к важному выводу.

Контекст типов никак не влияет на контекст значений

Контекст типов не участвует в логике функционала, не влияет на работу браузера, итоговый код одинаково работает как с ним, так и без него. Задача контекста типов — дать анализатору TypeScript данные для проверки соответствия значений описанным типам.

К этому правилу есть пара замечаний. Оператор typeof в контексте типов принимает в качестве операнда значение из контекста значений. Это не исключение из правила, так как контекст типов всё ещё не влияет контекст значений. Наоборот, контекст значений влияет на типизацию, давая последней интересные возможности привязки типов к конкретным значениям.

const obj = { one: 1, two: 2 };

type Fn = () => typeof obj;

const fn: Fn = () => 1 // Ошибка! Type 'number' is not assignable
                       // to type '{ one: number; two: number; }'
             

Код 1.4. Возвращаемое значение функции типа Fn, привязано к типу значения obj

Typeof написан внутри type. Он не работает по правилам JavaScript. А obj это обычное значение переменной. TypeScript определяет её тип и проверяет его соответствие возвращаемому значению функции fn. Он будет подсвечивать ошибку, пока мы не исправим значение obj или return функции.

const fn: Fn = () => (     
    { one: Infinity, two: NaN } // Теперь всё нормально!
)                               // P.S. Да-да, эти константы относятся к типу number

Если мы поменяем значение объекта obj и добавим новые свойства в него или изменим типы их значений, то это изменит тип Fn. Это не сломает работу кода, ведь Fn находится в контексте типов. Зато TypeScript подсветит ошибку не соответствия типа.

const obj = {
    one: 1,
    two: 'значение стало другого типа',
}

type Fn = () => typeof obj;

const fn1: Fn = () => ({
    one: Infinity,
    two: NaN, // Type 'number' is not assignable to type 'string'
})

Привязка типов к значениям важна при типизации проекта на TypeScript . В последующих статьях мы рассмотрим различные приёмы в этом подходе.

Другой особенностью языка TypeScript является тип перечисления enum. Этот тип хранит в своей структуре строковые или числовые (натуральные числа и ноль) константы. Особенный он тем, что используется как контексте типов, так и в контексте значений.

enum MyEnum {
    ZERO,
    ONE,
}

const fn = (num: MyEnum) => num; // используется в контексте типов

const one = fn(MyEnum.ONE); // используется в контексте значений

Код 1.5. Использование enum как тип и как значение

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

И вновь подобное поведение enum не является исключением из правила "Контекст типов никак не влияет на контекст значений. Мы можем убрать enum из контекста типов и поведение кода в браузере не изменится. Если же убрать enum из контекста значений, то это естественно изменит работу функции.

Аналогичным поведением обладают примитивные значения из JavaScript. Так как TypeScript использует литеральные типы, то в контекст типов можно записать эти значения:

const one: 1 = 1;             // Правильный код
const a: 'a' = 'b';           // Ошибка! Type '"b"' is not assignable to type '"a"'
const n: null = null;         // Правильный код
const u: undefined = void 0;  // Правильный код
const t: true = false;        // Ошибка! Type 'false' is not assignable to type 'true'.

Подобная типизация не имеет смысла на практике. TypeScript неявно задаёт тип переменной, ориентируясь на значение, которое ей присваивается. Чаще разработчики комбинируют литеральные или примитивные типы через знак |. И прежде чем погрузиться в эти тонкости в следующей статье будет описан подход в понимании типов, который по мнению автора не известен многим разработчикам.

Следующая статья: TypeScript в React-приложениях. 2. Как понимать типы.

Комментарии (9)


  1. XelaVopelk
    16.10.2022 09:36
    +1

    "Например, следующий пример функции на Javascript, включает проверку типа входных данных:" вряд ли пример, в котором код проверки больше основного тела функции, кого-то воодушевит на такое. А если будет не один параметр, а пять? А если не чиселко, а сложный объект?


    1. Svetozarpnz Автор
      16.10.2022 11:25

      Дельное замечание. Спасибо!
      В придумывании примеров преследую мысль, что он должен быть компактным и передавать идею посыла в тексте. Сам понимаю, что иногда это далеко от того, что мы встречаем на практике.


  1. DenisGummi
    16.10.2022 11:26
    -1

    Классно пишешь, о некоторых фишках я как то не задумывался. А в конце еще и заинтриговал. Жду продолжения


    1. Svetozarpnz Автор
      16.10.2022 11:34
      -1

      Спасибо! Уже приступил.
      Надеюсь, даже техническую литературу удастся сделать захватывающей.


  1. Yasna12
    17.10.2022 09:07

    Прекрасная статья! Первая часть, которая объясняет - зачем TypeScript - самая важная:)


  1. lgick
    18.10.2022 16:41

    Использовал ts в одном из проектов. Скорость разработки снизилась, количество эксепшенов возросло. Vim долго настраивал, так и не настроил. К npm модулям стало нужно подгружать типы

    Я не сталкивался с функциями, которые вложены многократно друг в друга, о которых пишет автор. Допускаю, это возможно, но не считаю такой код хорошей практикой.

    Я не встречал серьёзных ошибок из-за типов. Если писать код аккуратно, именовать переменные должным образом, разбивать код на компоненты, ошибок можно избежать.

    Ну и в реакте есть PropTypes.


    1. Svetozarpnz Автор
      18.10.2022 16:58

      Спасибо. Ваш посыл следует отнести в коллекцию неприятий TypeScript'а разработчиками.

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

      Сожалею, что был недостаточно убедителен для вас, призывая использовать TypeScript в крупных приложениях.


  1. FrameMuse
    18.10.2022 16:41

    А как это относится к React?


    1. Svetozarpnz Автор
      18.10.2022 16:45

      В планируемом цикле статей, я буду опираться на особенности типизации React-приложений. В текущей статье рассматриваются общие сведения и нет акцента на конкретной библиотеке.