В программировании встречаются ситуации, когда мы работаем с ограниченным набором значений какого-то типа, например, c определенными строками. В них могут входить справочные данные, статусы и так далее. Как мог бы выглядеть статус заказа:
Created
Paid
Shipped
Delivered
Код, который работает с этими данными, будет сохранять их в базу данных, отправлять и получать по сети и проверять статус заказа.
Если использовать всегда только общие типы для таких данных, например, string
, то мы лишимся многих преимуществ, например:
Для решения этой задачи TypeScript поддерживает литеральный тип. Они представляют множество, состоящее только из одного элемента. Они доступны только для следующих типов: string
, boolean
, number
и BigInt
:
type Hexlet = 'hexlet';
type One = 1;
type False = false;
type BigN = 100n;
С точки зрения теории множеств такой тип представляет собой множество, которое состоит из одного элемента. А для системы типов это ограничение — переменной не может быть присвоено ничего, кроме указанного значения:
type TestValue = 'test';
let test: TestValue = 'test';
test = 'string'; // Error: Type '"string"' is not assignable to type '"test"'.
Используя объединение типов, мы можем получить тип, который принимает только нужные нам значения:
type OrderStatus = 'Created' | 'Paid' | 'Shipped' | 'Delivered';
Также литеральные типы могут комбинироваться с любыми другими типами. Так мы можем получить ограничение, под которое попадают все числа и false
:
type NumberFalse = number | false;
Проблема, описанная в этом уроке, в большинстве языков реализуется через перечисления, которые также добавлены в TypeScript:
enum OrderStatus {
Created = 'Created',
Paid = 'Paid',
Shipped = 'Shipped',
Delivered = 'Delivered',
}
Но в TypeScript с перечислениями не все так хорошо.
TypeScript — это надстройка над JavaScript, которая добавляет типы, но не изменяет сам язык.
В случае с Enum — это не так. Перечисления — это конструкция языка, которая остается существовать в коде после трансляции кода в JavaScript.
По этой причине некоторые разработчики используют вместо них Union Types, которые позволяют сделать практически то же самое с помощью строковых литералов.
При этом все равно рекомендуется использовать Enum в прикладном коде, так как это дает дополнительные гарантии надежности. А в коде библиотек использовать Union Types, так как это более гибко и дает дополнительные возможности.
При конфигурации библиотеками нам встречаются случаи, когда от нас ожидают одну из строк. Например, дают выбор из нескольких баз данных:
const dataSourceConfig = {
type: 'postgre', // может также быть mysql
host: 'localhost',
port: 5432,
};
const AppDataSource = new DataSource(dataSourceConfig)
Для описания таких объектов используется тип объектных литералов, где поля инициализируются одним литеральным типом или их пересечением:
type DataSourceOption = {
type: 'postgre' | 'mysql';
host: string;
port: number;
}
С помощью такого типа мы можем гарантировать, что передаваемый объект будет содержать только одно из двух значений в поле type
, что выступает одновременно и документацией, и ограничением.
Это дает авторам библиотек дополнительный инструмент документации, а разработчикам крутой автокомплит, а также уберегает их от ошибок в передаваемых аргументах.
В случае с объектами конфигурации часто мы не хотим, чтобы их меняли извне, и ожидаем конкретных значений внутри. Здесь нам на помощь приходит приведение типа к литеральному через Type Assertion as const
:
const ormConfig = {
type: 'mysql',
host: 'localhost',
port: 5432,
} as const;
На выходе мы получаем тип с неизменяемыми (readonly
) полями и литеральными типами в значении. Такая техника также применима к массивам. Она превращает их в кортежи — массивы фиксированной длины, также защищенные от изменений. И также применима к простым типам, например, string
:
const str = 'test' as const;
type Str = typeof str; // 'test'
Таким образом мы можем создавать типы, которые будут содержать только определенные значения. Это позволяет нам получить дополнительные гарантии от компилятора и упростить работу с кодом.
Реализуйте функцию makeTurn()
, которая принимает строку left
или right
и перемещает черепашку вперед-назад по одномерному массиву фиксированного размера с пятью элементами. Если черепашка выходит за пределы массива, то выбрасывается исключение.
const { makeTurn, state } = startGame();
console.log(state); // ['turtle', null, null, null, null]
makeTurn('left') // ERROR
makeTurn('right');
makeTurn('right');
console.log(state); // [null, null, 'turtle', null, null]
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1import * as ta from 'type-assertions';
2
3import startGame from './index';
4
5test('startTurtleGame', () => {
6 const { makeTurn, state } = startGame();
7
8 expect(state).toEqual(['turtle', null, null, null, null]);
9
10 expect(() => makeTurn('left')).toThrow();
11 expect(state).toEqual(['turtle', null, null, null, null]);
12
13 makeTurn('right');
14 expect(state).toEqual([null, 'turtle', null, null, null]);
15
16 makeTurn('right');
17 makeTurn('right');
18 expect(state).toEqual([null, null, null, 'turtle', null]);
19
20 makeTurn('right');
21 expect(state).toEqual([null, null, null, null, 'turtle']);
22
23 expect(() => makeTurn('right')).toThrow();
24
25 makeTurn('left');
26 expect(state).toEqual([null, null, null, 'turtle', null]);
27
28 ta.assert<ta.Equal<ReturnType<typeof makeTurn>, void>>();
29});
30
Решение учителя откроется через: