Если у вас есть класс с литералами, вы можете использовать это свойство для разметки частей объединения и их отличия.
В качестве примера рассмотрим объединение Square
и Rectangle
, здесь у нас есть элемент kind
, который существует в обоих частях объединения и имеет определенный литеральный тип:
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
Если вы используете стиль проверки - защита типа, а именно сравнение (==
, ===
, !=
, !==
) или switch
со свойством отличия (здесь kind
), то TypeScript поймет, что объект должен иметь тип, который имеет этот конкретный литерал и уточнит для вас тип :)
function area(s: Shape) {
if (s.kind === "square") {
// Теперь TypeScript *знает*, что `s` должен быть квадратом ;)
// Так что вы можете безопасно использовать его свойства :)
return s.size * s.size;
}
else {
// Не квадрат? Что ж, TypeScript определит, что это должен быть прямоугольник ;)
// Так что вы можете безопасно использовать его свойства :)
return s.width * s.height;
}
}
Довольно часто вы хотите убедиться, что у всех частей объединения есть какой-то код (действие) обрабатывающее их.
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;
В качестве примера того, где что-то идет не так:
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. Например, если вы добавите тщательную проверку, вы получите красивую ошибку:
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;
}
}
Это заставляет вас обработать этот новый вариант:
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
:
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 и выполнении тщательных проверок TypeScript может пожаловаться, что "не все пути кода возвращают значение". Вы можете отключить это, просто вернув переменную _exhaustiveCheck
(типа never
). Вот так:
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
), а затем выбрасывает ошибку, если ее тело когда-либо выполняется:
function assertNever(x:never): never {
throw new Error('Неожиданное значение. Не должно было быть never.');
}
Пример использования с функцией вычисления площади:
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);
}
}
Допустим, у вас есть структура данных в форме:
type DTO = {
name: string
}
И после того, как у вас есть куча таких DTO
, вы понимаете, что name
было плохим выбором. Вы можете добавить управление версиями ретроспективно, создав новое объединение с литеральным номером (или строкой, если хотите) для DTO. Отметьте версию 0 как undefined
, и если у вас включен strictNullChecks, это сработает:
type DTO =
| {
version: undefined, // версия 0
name: string,
}
| {
version: 1,
firstName: string,
lastName: string,
}
// Ещё позднее
| {
version: 2,
firstName: string,
middleName: string,
lastName: string,
}
// И так далее
Пример использования такого DTO:
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 с добавленными описаниями типов TypeScript:
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 обеспечивает защиту от опечаток, повышенную способность к рефакторингу и самодокументируемый код.