Перейти к содержанию

Размеченные объединения

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

В качестве примера рассмотрим объединение Square и Rectangle, здесь у нас есть элемент kind, который существует в обоих частях объединения и имеет определенный литеральный тип:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Square {
    kind: 'square';
    size: number;
}

interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

Если вы используете стиль проверки - защита типа, а именно сравнение (==, ===, !=, !==) или switch со свойством отличия (здесь kind), то TypeScript поймет, что объект должен иметь тип, который имеет этот конкретный литерал и уточнит для вас тип :)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function area(s: Shape) {
    if (s.kind === 'square') {
        // Теперь TypeScript *знает*, что `s` должен быть квадратом ;)
        // Так что вы можете безопасно использовать его свойства :)
        return s.size * s.size;
    } else {
        // Не квадрат? Что ж, TypeScript определит, что это должен быть
        // прямоугольник ;)
        // Так что вы можете безопасно использовать его свойства :)
        return s.width * s.height;
    }
}

Тщательные проверки

Довольно часто вы хотите убедиться, что у всех частей объединения есть какой-то код (действие) обрабатывающее их.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
interface Square {
    kind: 'square';
    size: number;
}

interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
// Кто-то только что добавил этот новый тип `Circle`
// Мы бы хотели, чтобы TypeScript выдавал ошибку везде где это *нужно*,
// для её исправления
interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Rectangle | Circle;

В качестве примера того, где что-то идет не так:

1
2
3
4
5
6
7
8
9
function area(s: Shape) {
    if (s.kind === 'square') {
        return s.size * s.size;
    } else if (s.kind === 'rectangle') {
        return s.width * s.height;
    }
    // Было бы здорово, если бы вы могли заставить TypeScript выдавать
    // вам ошибку?
}

Вы можете сделать это, просто добавив переход к следующему условию в списке(при отсутствии совпадения) и убедившись, что предполагаемый тип в этом блоке совместим с типом never. Например, если вы добавите тщательную проверку, вы получите красивую ошибку:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function area(s: Shape) {
    if (s.kind === 'square') {
        return s.size * s.size;
    } else if (s.kind === 'rectangle') {
        return s.width * s.height;
    } else {
        // ОШИБКА: `Circle` нельзя присвоить `never`
        const _exhaustiveCheck: never = s;
    }
}

Это заставляет вас обработать этот новый вариант:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function area(s: Shape) {
    if (s.kind === 'square') {
        return s.size * s.size;
    } else if (s.kind === 'rectangle') {
        return s.width * s.height;
    } else if (s.kind === 'circle') {
        return Math.PI * s.radius ** 2;
    } else {
        // Okay еще раз
        const _exhaustiveCheck: never = s;
    }
}

Switch

СОВЕТ: конечно, вы также можете сделать это помощью инструкции switch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.width * s.height;
        case 'circle':
            return Math.PI * s.radius * s.radius;
        default:
            const _exhaustiveCheck: never = s;
    }
}

strictNullChecks

При использовании strictNullChecks и выполнении тщательных проверок TypeScript может пожаловаться, что "не все пути кода возвращают значение". Вы можете отключить это, просто вернув переменную _exhaustiveCheck (типа never). Вот так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.width * s.height;
        case 'circle':
            return Math.PI * s.radius * s.radius;
        default:
            const _exhaustiveCheck: never = s;
            return _exhaustiveCheck;
    }
}

Добавьте тщательные проверки

Вы можете написать функцию, которая принимает never (и поэтому может быть вызвана только с переменной, которая логически выводится как never), а затем выбрасывает ошибку, если ее тело когда-либо выполняется:

1
2
3
4
5
function assertNever(x: never): never {
    throw new Error(
        'Неожиданное значение. Не должно было быть never.'
    );
}

Пример использования с функцией вычисления площади:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Square {
    kind: 'square';
    size: number;
}
interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.width * s.height;
        // Если во время компиляции добавлен новый кейс, вы получите
        // ошибку компиляции
        // Если новое значение появится во время выполнения, вы получите
        // ошибку во время выполнения
        default:
            return assertNever(s);
    }
}

Ретроспективное управление версиями

Допустим, у вас есть структура данных в форме:

1
2
3
type DTO = {
    name: string;
};

И после того, как у вас есть куча таких DTO, вы понимаете, что name было плохим выбором. Вы можете добавить управление версиями ретроспективно, создав новое объединение с литеральным номером (или строкой, если хотите) для DTO. Отметьте версию 0 как undefined, и если у вас включен strictNullChecks, это сработает:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type DTO =
    | {
          version: undefined; // версия 0
          name: string;
      }
    | {
          version: 1;
          firstName: string;
          lastName: string;
      }
    // Ещё позднее
    | {
          version: 2;
          firstName: string;
          middleName: string;
          lastName: string;
      };
// И так далее

Пример использования такого DTO:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function printDTO(dto: DTO) {
    if (dto.version == null) {
        console.log(dto.name);
    } else if (dto.version == 1) {
        console.log(dto.firstName, dto.lastName);
    } else if (dto.version == 2) {
        console.log(
            dto.firstName,
            dto.middleName,
            dto.lastName
        );
    } else {
        const _exhaustiveCheck: never = dto;
    }
}

Redux

Популярная библиотека, которая использует это - redux.

Вот суть redux с добавленными описаниями типов TypeScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { createStore } from 'redux';

type Action =
    | {
          type: 'INCREMENT';
      }
    | {
          type: 'DECREMENT';
      };

/**
 * Это редюсер, чистая функция с сигнатурой (состояние, действие) => состояние .
 * Он описывает, как действие преобразует состояние в следующее состояние.
 *
 * Форма состояния зависит от вас: это может быть примитив, массив, объект,
 * или даже структура данных Immutable.js. Единственная важная часть - вы должны
 * не изменять объект состояния, а возвращать новый объект, если состояние
 * изменяется.
 *
 * В этом примере мы используем оператор switch и строки, но вы можете
 * использовать хелпер, который
 * следует другому соглашению (например, маппинг функций), если это имеет
 * смысл для вашего проекта.
 */
function counter(state = 0, action: Action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}

// Создаем хранилище Redux, в котором хранится состояние вашего приложения.
// Его API: {subscribe, dispatch, getState}.
let store = createStore(counter);

// Вы можете использовать subscribe() для обновления пользовательского
// интерфейса в ответ на изменения состояния.
// Обычно вы бы использовали библиотеку привязки представления (например,
// React Redux), а не subscribe() напрямую.
// Однако также может быть удобно сохранить текущее состояние в localStorage.

store.subscribe(() => console.log(store.getState()));

// Единственный способ изменить внутреннее состояние - отправить действие.
// Действия могут быть сериализованы, залогированы или сохранены, а затем
// воспроизведены.
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

Использование redux с TypeScript обеспечивает защиту от опечаток, повышенную способность к рефакторингу и самодокументируемый код.

Комментарии