Как стать автором
Обновить

TypeScript в React-приложениях. 2. Как понимать типы

Время на прочтение5 мин
Количество просмотров5.4K

Часто разработчики воспринимают типы как набор отличительных особенностей переменной. Это поверхностное видение мешает общему пониманию работы Typescript и поведению его анализатора. В результате приходится привыкать к разным приёмам типизации, вместо того, чтобы сделать для себя их очевидными.

Прежде чем приступить к рассмотрению приёмов типизации, привьём себе немного более глубокий взгляд на типы и тогда требования языка Typescript к коду станут выглядеть более естественными.

Содержание

Тип — это множество значений

Когда мы задаём тип для переменной, то мы сообщаем ей, что она может принять любое значение из множества, которое включает этот тип. К примеру, тип string включает почти бесконечное множество строк, а тип number включает огромное множество чисел и значения NaN, Infinity, -Infinity.

Изображение 2.1 Типы как множество значений
Изображение 2.1 Типы как множество значений

Во множестве значений типа boolean всего два значения (true и false), а в типе null — всего одно значение. И это не отменяет понятие множества. Особенно хорошо данный подход воспринимается, когда мы начинаем комбинировать типы.

С помощью оператора объединения типов можно объединить типы и создать новый.

type NumStr = string | number;

Два множества значений исходных типов объединятся в одно множество.

Изображение 2.2. Объединение типов в представлении единого множества
Изображение 2.2. Объединение типов в представлении единого множества

Создадим новый тип enum, который будет хранить числовые значения

enum Numeric {
  ONE = 1,
  TWO, 
  THREE,
}

Тип Numeric содержит множество из трёх значений: 1, 2, 3. И это множество является подмножеством типа number.

Изображение 2.3. Числовой enum является подмножеством типа number
Изображение 2.3. Числовой enum является подмножеством типа number

По сути любые числовые enum будут принадлежать множеству number. Соответственно строчные enum принадлежат типу string. Принадлежность подмножества множеству обозначается знаком ⊂. Numericnumber, numbernumber | string и т. п.

Авторское отступление

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

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

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

Утиная типизация

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

Представим, что добыча (утка а может быть даже олень) - это программа, а охотник - программист. Задача охотника приманить животное схожа с задачей программиста заставить программу следовать определенному поведению. Для этого охотник использует свисток, имитирующий голос животного, а также и нейтрализаторы человеческого запаха и одежду, чтобы слиться с природой и не спугнуть добычу.

Утка или другое животное идут на зов, как и программа следует определённому поведению, потому что соблюдены особенности передаваемых ей переменных, то есть типы.

Изображение 2.4 Морское чудовище передаёт в функцию ship() переменную tail, которая имеет тип NudeWoman.
Изображение 2.4 Морское чудовище передаёт в функцию ship() переменную tail, которая имеет тип NudeWoman.

Переменные объектного типа должны обязательно иметь те свойства, которые используются в коде. Typescript помогает за этим следить. Что вовсе не означает, что эти же переменные не могут иметь других свойств. На рисунке 2.4 чудовище использует утиную типизацию, чтобы корабль смог выполнить одну из своих функций - спасение утопающих. Так и разработчик передаёт в код переменные подходящего типа, заставляя работать его в нужном русле.

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

<div ref={() => onDivRender()} />

Код 2.1 передача в ref-атрибут колбэк-функции с целью отследить момент рендера элемента

Комбинирование объектных типов

Рассмотрим следующий пример типизации.

interface WithId {
  id: string;
}

interface UserData extends WithId {
  name: string;
}

Код 2.2. Наследование типов

Тип WithId, полученный через interface синтаксис, согласно утиной типизации включает почти бесконечное множество значений. Эти значения являются объектами с разнообразными свойствами, среди которых обязательно должно быть id со значением типа string.

Тип UserData расширяет тип WithId с точки зрения количества свойств. Но с точки зрения значений тип UserData более узкий, чем WithId и является его подмножеством.

Изображение 2.5. Тип UserData  — подмножество типа WithId
Изображение 2.5. Тип UserData — подмножество типа WithId

Тип WithId включает также огромное количество значений, но в отличии от UserData свойство name может быть другого типа, не равного string, или вовсе отсутствовать. Возникает терминологический парадокс, который может сбивать с толку. Добавляя новые свойства в тип, мы расширяем его отличительные особенности, но сужаем количество значений, которые ему подходят.

Наследование типов через type синтаксис происходит с помощью знака пересечения типов:

interface WithId {
  id: string;
}

type UserData = WithId & {
  name: string;
}

Код 2.3. Пересечение типов

Когда мы ищем пересечение объектных типов, мы собираем их свойства вместе, что может показаться странным. Потому что когда мы ищем пересечения массивов (значений), то в результате получаем массив только с теми элементами, которые присутствуют в обоих исходных массивах. Почему это не работает с объектными типами?

Тип с описанным свойством id является множеством объектов с разными свойствами, в том числе свойством name типа string. Тип с описанным свойством name, так же включает множество разных объектов, в том числе свойство id типа string. В итоге большей частью эти типы пересекаются. Каждое значение в пересечении должно относиться как к типу WithId, т.е. иметь свойство id, так и к типу UserData, т.е. иметь свойство name. И таких значений так же великое множество.

Изображение 2.6. Пересечение типов WithId и { name: string } создаёт тип UserData
Изображение 2.6. Пересечение типов WithId и { name: string } создаёт тип UserData

Что если мы попробуем найти пересечения объектных типов с одинаковыми именами свойств, но разными типами:

type StringId = {
  id: string;
}

type NumberId = {
  id: number;
}

type WithId = NumberId & StringId;

Код 2.4. Пересечение типов с одинаковыми свойствами

Множества string и number не имеют пересечений (см. изобр. 2.1), значит мы не сможем подобрать такой объект, который удовлетворял бы особенностям обоих исходных типов. И тип WithId будет равен типу never.

У типа never отсутствует множество значений. Иногда анализатор Typescript подсвечивает для вашей переменной тип never. Это означает, что в логике вашего кода он не может найти множества, которое соответствует переменной. Чья эта проблема?

В следующих примерах рассмотрены некоторые ситуации, когда тип переменной определяется как never:

// функция, выбрасывающая исключение

const fn = () => {
  throw new Error()
}
const result = fn(); // тип result - never


// функция, c бесконечным циклом

const fn2 = () => {
  while(true) {}
}
const result2 = fn2(); // тип result2 - never


// функция, в который объединенный тип сужается благодаря защитникам типа

function getTypeText(id: string | number) {
  if(typeof id === 'string') {
    return `${id}-это строка`; // тип id — string
  }
  if(typeof id === 'number') {
    return `${id}-это число`; // тип id — number
  }
  return `${id}-сюда выполнение никогда не дойдет`; // тип id — never
}

Код 2.5. Примеры, когда переменная будет иметь тип never

Во всех этих случаях анализатор видит, что инструкция в которой используется переменная, не будет выполнена. С точки зрения типизации это неверно спроектированный код и как следствие потенциально содержит ошибки.

Типы any и unknown с точки зрения множеств противоположны never. Переменные этих типов могут принимать значения из множества, которое включает все остальные. Различаются эти типы тем, что на них по-разному реагирует анализатор.

Рассмотрев базовые особенности синтаксиса Typescript и понимание типов как множеств мы перейдём к описанию способов, соглашений и предложений в типизации приложения.

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

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии4

Публикации