Зарегистрируйтесь, чтобы продолжить обучение

Иерархия типов Основы Typescript

В этом уроке мы разберем связь между типами, которая выстраивается в иерархию.

Типы как подмножества

Рассмотрим пример ошибки Type X is not assignable to type Y в функции для сортировки элементов. Допустим, у нас уже написана функция sort. И чтобы описать только ее типы, воспользуемся ключевым словом declare:

type ComparatorCallback = (item1: number, item2: number, index: number) => -1 | 0 | 1
declare function sort(arr: Array<number>, callback: ComparatorCallback): Array<number>

const arr = [1, 2, 3];
const comparator = (item1: number, item2: number) => Math.sign(item1 - item2);

sort(arr, comparator) // Error: Type 'number' is not assignable to type '0 | 1 | -1'.

Проверка типов выдала ошибку: объединение литеральных типов 0 | 1 | -1 не совместимо с типом number. Можно подумать, что система типов ошибается, и стоит использовать any. Но если мы подумаем о литеральных числовых типах как о подмножествах number, все становится логично.

Здесь отчетливо просматривается связь типов с теорией множеств. Множество A является подмножеством B, если любой элемент, который принадлежит A, также принадлежит B. Так мы получаем связи между типами, которые выстраиваются в иерархию типов. Это помогает понять, возможно ли присвоить переменную одного типа переменной другого типа.

Литеральные типы

Напомним, что литеральные типы существуют для четырех типов данных:

  • boolean
  • string
  • number
  • BigInt

В итоге любой литеральный тип можно присвоить переменной соответствующего типа:

let num: number = 1;
const two: 2 = 2;
const notTrue: false = false;

num = two;
num = notTrue; // Type 'boolean' is not assignable to type 'number'.

Здесь 2 используется как литеральный тип, который представляет собой множество из одного элемента — двойки.

Анализатор успешно пропустил присваивание литерального типа числа к number, но литеральный boolean тип мы уже не смогли присвоить. Чтобы решить эту проблему, можно использовать объединение типов number | boolean. Но если мы не уверены, что может быть присвоено, нам пришлось бы делать объединение с потенциально огромным числом типов.

В этом случае нам на помощь приходит тип unknown.

Тип unknown

Тип unknown — это надмножество всех доступных типов. Он позволяет присвоить переменной значение произвольного типа:

let unknownValue: unknown = 1;
const two: 2 = 2;
const notTrue: false = false;

unknownValue = two;
unknownValue = notTrue; // OK

Может показаться, что тип unknown работает так же, как any. Однако между ними есть различие. Тип any по сути отключает проверку типов и позволяет выполнять любые операции со значением, например, обращаться к свойствам переменной. Тип unknown запрещает это и требует предварительной проверки типа переменной, либо приведения к нужному типу.

Далее разберем случай, когда нам не нужно присваивать переменной никакого значения.

Тип never

Иногда на практике нужно быть уверенным, что переменной не будет присвоено никакого значения. Это можно реализовать с помощью типа never:

let neverValue: never;
const two: 2 = 2;

neverValue = two; // Type 'number' is not assignable to type 'never'

Множества типов

Из текущих знаний мы можем составить следующую картинку множеств типов TypeScript:

NumberOrString

В множество number также входят все объединения литеральных типов чисел, а в множество string — литеральных строк:

type NumberUnion = -2 | -1 | 1 | 2

const one: NumberUnion = 1;
const num: number = one;

type StringUnion = 'a' | 'b' | 'c' | 'd'

const aChar: StringUnion = 'a';
const str: string = aChar;

Такое подмножество типов называют подтипом, а само множество супертипом.

Взаимосвязи подтипов и супертипов — ключевая концепция любого статически типизированного языка. Они образуют иерархию типов. Это становится особо важно, когда мы хотим привести один тип к другому.

Приведение типов

Рассмотрим различные варианты приведения типов:

let num = 1; // Неявное восходящее приведение
let one: number = 1; // Явное восходящее приведение

let two = num as 2; // Явное нисходящее приведение

let three = 3 as const; // Приведение к литеральному типу — нисходящее

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

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

Разберем еще один пример:

const args = [8, 5]; // args: number[]
const angle = Math.atan2(...args); // error! A spread argument must either have a tuple type or be passed to a rest parameter.
console.log(angle);

Здесь компилятор определяет переменной args как тип number[] — массив с любым количеством числовых элементов. Компилятор расширил возможные значения в массиве, несмотря на то, что мы указали всего два элемента в массиве. Это и есть неявное восходящее приведение типа, когда компилятор приводит к более общему типу.

По этой причине возникает ошибка, потому что метод Math.atan2() ожидает два аргумента, а тип переменной args может содержать любое количество элементов. Исправим это с помощью as:

const args = [8, 5] as const; // readonly [8, 5]
const angle = Math.atan2(...args); // okay
console.log(angle);

Теперь компилятор определяет тип для переменной args как литеральный тип [8, 5]. Хоть он и является множеством типа number[], но это уже более строгий тип, который является массивом из двух конкретных чисел, поэтому ошибки не будет. Такое приведение называется нисходящим, потому что мы от более широкого типа приводим к более узкому типу, содержащему меньшее количество возможных значений.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 26 декабря
профессия
от 39 525 ₸ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 26 декабря
профессия
от 25 000 ₸ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»