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. Так мы получаем связи между типами, которые выстраиваются в иерархию типов. Это помогает понять, возможно ли присвоить переменную одного типа переменной другого типа.
Литеральные типы
Напомним, что литеральные типы существуют для четырех типов данных:
booleanstringnumberBigInt
В итоге любой литеральный тип можно присвоить переменной соответствующего типа:
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;
unknownValue = 2; // OK
unknownValue = false; // OK
unknownValue = "string"; // OKМожет показаться, что тип unknown работает так же, как any. Однако между ними есть различие. Тип any по сути отключает проверку типов и позволяет выполнять любые операции со значением, например, обращаться к свойствам переменной. Тип unknown запрещает это и требует предварительной проверки типа переменной, либо приведения к нужному типу:
let unknownValue: unknown;
unknownValue = "string";
unknownValue.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'unknown'.Здесь мы смогли присвоить переменной unknownValue строку, но не можем вызвать метод toUpperCase(), потому что компилятор не знает, что в переменной находится строка. Чтобы вызвать метод, нужно сначала проверить тип переменной.
Другое важное свойство: при объединении любых типов с unknown мы всегда получаем unknown:
type UnionWithUnknown = unknown | number | boolean;Так происходит, потому что unknown — это надмножество всех типов, поэтому любое объединение с ним дает его само. Исключение тут any: он и в этом случае отключает проверку типов и не подчиняется модели типов как множеств.
Далее разберем случай, когда нам не нужно присваивать переменной никакого значения.
Тип never
Иногда на практике нужно быть уверенным, что переменной не будет присвоено никакого значения. Это можно реализовать с помощью типа never:
let neverValue: never;
const two: 2 = 2;
neverValue = two; // Type 'number' is not assignable to type 'never'Ошибка возникает, потому что never — это пустое множество, в котором нет ни одного элемента. Поэтому переменной neverValue нельзя присвоить никакое значение.
Множества типов
Из текущих знаний мы можем составить следующую картинку множеств типов TypeScript:

В множество 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;Такое подмножество типов называют подтипом, а само множество супертипом.
Взаимосвязи подтипов и супертипов — ключевая концепция любого статически типизированного языка. Они образуют иерархию типов. Это становится особо важно, когда мы хотим привести один тип к другому.
Приведение типов
Рассмотрим различные варианты приведения типов:
const num = 1; // Неявное восходящее приведение
const one: number = 1; // Явное восходящее приведение
const two = num as 2; // Явное нисходящее приведение
const 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 может содержать любое количество элементов. Приведение типов здесь не нужно: достаточно сразу назвать тип кортежем, и компилятору будет нечего расширять:
const args: [number, number] = [8, 5];
const angle = Math.atan2(...args); // okay
console.log(angle);Кортеж фиксирует количество элементов, но не запрещает их менять, а типом каждого элемента остается number:
args[0] = 100; // okayЕсли же значения меняться не должны, применяют нисходящее приведение as const:
const args = [8, 5] as const; // readonly [8, 5]
args[0] = 100; // Error: Cannot assign to '0' because it is a read-only property.Теперь тип переменной — не «два числа», а «восьмерка и пятерка, которые нельзя переписать». Это множество меньше, чем number[], поэтому приведение и называется нисходящим: мы перешли от более широкого типа к более узкому, содержащему меньшее количество возможных значений.
Аннотация кортежем и as const решают разные задачи. Первая называет тип, который нам нужен. Вторая запрещает компилятору расширять то, что уже написано в коде.
Задание
Реализуйте функцию getUserFriends(userResponseJSON, userId), которая принимает на вход JSON-строку и userId пользователя. JSON содержит массив пользователей users и массив друзей friends в виде пар [userId, userId]. Функция возвращает список друзей пользователя по переданному `userId``.
Если пользователь с указанным id не найден, то функция должна вернуть пустой массив.
const userJson = JSON.stringify({
users: [
{ id: 1, name: "John", age: 20 },
{ id: 2, name: "Mary", age: 21 },
],
friends: [[1, 2]],
});
getUserFriends(userJson, 1); // [{ id: 2, name: 'Mary', age: 21 }]
getUserFriends(userJson, 2); // [{ id: 1, name: 'John', age: 20 }]
getUserFriends(userJson, 3); // []Полезное
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. Так мы получаем связи между типами, которые выстраиваются в иерархию типов. Это помогает понять, возможно ли присвоить переменную одного типа переменной другого типа.
Литеральные типы
Напомним, что литеральные типы существуют для четырех типов данных:
booleanstringnumberBigInt
В итоге любой литеральный тип можно присвоить переменной соответствующего типа:
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;
unknownValue = 2; // OK
unknownValue = false; // OK
unknownValue = "string"; // OKМожет показаться, что тип unknown работает так же, как any. Однако между ними есть различие. Тип any по сути отключает проверку типов и позволяет выполнять любые операции со значением, например, обращаться к свойствам переменной. Тип unknown запрещает это и требует предварительной проверки типа переменной, либо приведения к нужному типу:
let unknownValue: unknown;
unknownValue = "string";
unknownValue.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'unknown'.Здесь мы смогли присвоить переменной unknownValue строку, но не можем вызвать метод toUpperCase(), потому что компилятор не знает, что в переменной находится строка. Чтобы вызвать метод, нужно сначала проверить тип переменной.
Другое важное свойство: при объединении любых типов с unknown мы всегда получаем unknown:
type UnionWithUnknown = unknown | number | boolean;Так происходит, потому что unknown — это надмножество всех типов, поэтому любое объединение с ним дает его само. Исключение тут any: он и в этом случае отключает проверку типов и не подчиняется модели типов как множеств.
Далее разберем случай, когда нам не нужно присваивать переменной никакого значения.
Тип never
Иногда на практике нужно быть уверенным, что переменной не будет присвоено никакого значения. Это можно реализовать с помощью типа never:
let neverValue: never;
const two: 2 = 2;
neverValue = two; // Type 'number' is not assignable to type 'never'Ошибка возникает, потому что never — это пустое множество, в котором нет ни одного элемента. Поэтому переменной neverValue нельзя присвоить никакое значение.
Множества типов
Из текущих знаний мы можем составить следующую картинку множеств типов TypeScript:

В множество 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;Такое подмножество типов называют подтипом, а само множество супертипом.
Взаимосвязи подтипов и супертипов — ключевая концепция любого статически типизированного языка. Они образуют иерархию типов. Это становится особо важно, когда мы хотим привести один тип к другому.
Приведение типов
Рассмотрим различные варианты приведения типов:
const num = 1; // Неявное восходящее приведение
const one: number = 1; // Явное восходящее приведение
const two = num as 2; // Явное нисходящее приведение
const 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 может содержать любое количество элементов. Приведение типов здесь не нужно: достаточно сразу назвать тип кортежем, и компилятору будет нечего расширять:
const args: [number, number] = [8, 5];
const angle = Math.atan2(...args); // okay
console.log(angle);Кортеж фиксирует количество элементов, но не запрещает их менять, а типом каждого элемента остается number:
args[0] = 100; // okayЕсли же значения меняться не должны, применяют нисходящее приведение as const:
const args = [8, 5] as const; // readonly [8, 5]
args[0] = 100; // Error: Cannot assign to '0' because it is a read-only property.Теперь тип переменной — не «два числа», а «восьмерка и пятерка, которые нельзя переписать». Это множество меньше, чем number[], поэтому приведение и называется нисходящим: мы перешли от более широкого типа к более узкому, содержащему меньшее количество возможных значений.
Аннотация кортежем и as const решают разные задачи. Первая называет тип, который нам нужен. Вторая запрещает компилятору расширять то, что уже написано в коде.
Задание
Реализуйте функцию getUserFriends(userResponseJSON, userId), которая принимает на вход JSON-строку и userId пользователя. JSON содержит массив пользователей users и массив друзей friends в виде пар [userId, userId]. Функция возвращает список друзей пользователя по переданному `userId``.
Если пользователь с указанным id не найден, то функция должна вернуть пустой массив.
const userJson = JSON.stringify({
users: [
{ id: 1, name: "John", age: 20 },
{ id: 2, name: "Mary", age: 21 },
],
friends: [[1, 2]],
});
getUserFriends(userJson, 1); // [{ id: 2, name: 'Mary', age: 21 }]
getUserFriends(userJson, 2); // [{ id: 1, name: 'John', age: 20 }]
getUserFriends(userJson, 3); // []Полезное
Ваше упражнение проверяется по этим тестам
import { expect, test } from "vitest";
import getUserFriends, { type UserResponse } from "./index";
test("getUserFriends", () => {
const userJson = JSON.stringify(<UserResponse>{
users: [
{ id: 1, name: "John", age: 20 },
{ id: 2, name: "Mary", age: 21 },
{ id: 3, name: "Peter", age: 22 },
{ id: 4, name: "Ann", age: 23 },
],
friends: [
[1, 2],
[1, 3],
[3, 2],
],
});
const friends = getUserFriends(userJson, 10);
expect(friends).toEqual([]);
const friends1 = getUserFriends(userJson, 1);
expect(friends1).toEqual([
{ id: 2, name: "Mary", age: 21 },
{ id: 3, name: "Peter", age: 22 },
]);
const friends2 = getUserFriends(userJson, 2);
expect(friends2).toEqual([
{ id: 1, name: "John", age: 20 },
{ id: 3, name: "Peter", age: 22 },
]);
const friends3 = getUserFriends(userJson, 3);
expect(friends3).toEqual([
{ id: 1, name: "John", age: 20 },
{ id: 2, name: "Mary", age: 21 },
]);
});Решение учителя откроется через:
20:00
