Выживание в экосистеме TypeScript - Часть 5: Защита типов и дискриминируемые союзы
Источник на Github: Демонстрация TypeScript
Небольшое примечание, прежде чем мы перейдем к этому: все примеры в этом посте используют TypeScript v2.9.1. Если вы видите другое поведение, проверьте свою версию. Время от времени я буду стараться обновлять примеры с помощью обновлений TypeScript.
Вступление
Это моя любимая функция TypeScript. Больше всего на свете это особенность, которая больше всего меняет то, как я пишу TypeScript, по сравнению с тем, как я пишу JavaScript. Эта функция позволяет писать надежный и простой для чтения полиморфный код, сохраняя при этом строгую безопасность типов. Например, в JavaScript очень часто пишут функции, которые принимают некоторое количество типов n в качестве возможных аргументов, скажем, функция может принимать либо строку, либо число, и работать разными способами в зависимости от переданных типов. Типы охраняются и различаются объединения позволяют сделать это, не попадая во все возможные проблемы, связанные с доступом к неправильному свойству или методу в зависимости от типа аргумента.
Начиная
Хотя этот пост может быть самостоятельным, это один пост из серии. Если вы хотите начать с самого начала, посмотрите: Написание безопасного для типов (ish) кода JavaScript. Самым важным является то, как я настроил свой проект, чтобы вы могли следить за примерами кода.
Введите охранников с оператором «is».
$ git checkout type-guards
Защита типа - отличный инструмент для работы с типами объединения или ненадежными данными. Защита типа - это просто функция, которая проверяет, принадлежит ли ее аргумент к какому-либо типу.
interface IRectangle { width: number height: number } interface ICircle { radius: number } type Shape = IRectangle | ICircle function isCicle(shape: Shape): shape is ICircle { return (shape as any).radius !== undefined }
Интересным моментом здесь является определение функции. Его возвращаемый тип - что-то странное: «shape is ICircle». Эта функция буквально проверяет, имеет ли наш аргумент shape тип ICircle. Тип возвращаемого значения очень буквален в том, что он тестирует. Проще говоря, функция возвращает логическое значение. Однако мы говорим TypeScript, что если это логическое значение истинно, у нас есть ICircle.
Это становится очень полезным, когда мы используем его в условном выражении.
const obj: Shape = { radius: 32 } if (isCicle(obj)) { console.log(`Radius: ${obj.radius}`) }
Когда защита типа возвращает true, TypeScript поймет и правильно проверит тип использование obj в качестве ICircle внутри блока if. Если вместо «obj.radius» мы попытаемся прочитать «obj.width», TypeScript выдаст ошибку времени компиляции.
Проверка недостоверных данных
Область, в которой я считаю, что защита типов особенно полезна, - это когда я проверяю полезную нагрузку из сетевого запроса или другого API, который возвращает что-то типа «любой». Я создам защиту типа, которая проверяет, соответствует ли полезная нагрузка тому, что я ожидал от этого API, прежде чем я позволю ей выйти и начать разрушать вещи в остальной части моего кода, где я приложил много усилий, чтобы убедиться, что все является типобезопасным. .
Еще лучшее решение для проверки полезной нагрузки от API, которое возвращает «любой», - это фактически не использовать необработанные данные, полученные от этого API.
interface IResponse { status: number user: { id: number name: string } } function validatedResponse(rawResponse: any): IResponse { if ( typeof rawResponse.status === 'number' && typeof rawResponse.user === 'object' && typeof rawResponse.user.id === 'number' && typeof rawResponse.user.name === 'string' ) { return { status: rawResponse.status, user: { id: rawResponse.user.id, name: rawResponse.user.name, } } } else { throw new Error('Unknown response type') } }
Почему я глубоко копирую объект, а не просто возвращаю rawResponse? Если я делаю это, потому что API потенциально ненадежен, возможно, могут быть дополнительные свойства, которые мне не нужны, что может вызвать проблемы позже, если я буду повторять этот объект. Лучше иметь самые надежные гарантии.
Примечание. Конечно, лучшее решение для моего примера, если вы имеете дело с большим количеством запросов API, - это использовать какой-то валидатор схемы и библиотеку глубокого копирования.
Дискриминационные союзы
$ git checkout discriminated-unions
Идея дискриминируемых союзов основывается на аккуратных типах защитников. С помощью защиты типа мы определяем, как мы проверяем, принадлежит ли объект определенному типу. Однако, если мы сможем предоставить подсказки для TypeScript, мы сможем получить эти проверки автоматически.
Давайте вернемся к интерфейсам фигур, которые мы видели ранее. Однако на этот раз давайте добавим немного дополнительной информации к интерфейсам, чтобы различать типы.
interface IRectangle { type: 'Rectangle' width: number height: number } interface ICircle { type: 'Circle' radius: number } type Shape = IRectangle | ICircle
В TypeScript буквальные значения могут использоваться как типы. Тип ICircle должен иметь свойство «тип», и значение этого свойства должно быть буквальной строкой «Circle». Это свойство типа отличает каждый из типов в объединении Shape. Поскольку значение свойства типа уникально для каждого типа в объединении форм, TypeScript может различать типы объединения на основе проверки этого свойства.
function area(shape: Shape): number { switch (shape.type) { case 'Rectangle': return shape.width * shape.height case 'Circle': return Math.PI * Math.pow(shape.radius, 2) } }
В каждом случае вышеупомянутого переключателя TypeScript знает, с каким типом из объединения Shape мы имеем дело, на основе свойства type. Таким образом, мы можем выполнять простой вид сопоставления типов.
Использование Never
Это подводит нас к очень полезному типу утилит в TypeScript - типу «никогда». Они никогда не используются для проверки того, что определенные части вашего кода не могут быть оценены во время выполнения. Зачем нам это нужно? Возвращаясь к нашей функции «area» для типов фигур, давайте добавим регистр по умолчанию.
function area(shape: Shape): number { switch (shape.type) { case 'Rectangle': return shape.width * shape.height case 'Circle': return Math.PI * Math.pow(shape.radius, 2) default: const msg: never = shape throw new TypeError(`Unknown type: ${msg}`) } }
Поскольку мы используем тип never в случае по умолчанию, TypeScript подтвердит, что выполнение варианта по умолчанию невозможно. Это гарантирует, что наши случаи являются исчерпывающими для типов в объединении Shape.
Чтобы проиллюстрировать ценность этого. Давайте добавим тип к нашему объединению Shape.
interface ITriangle { type: 'Triangle' side: number } type Shape = IRectangle | ICircle | ITriangle
Теперь, если мы попробуем скомпилировать наш код.
$ npm run build Type 'ITriangle' is not assignable to type 'never'.
Компилятор сообщает нам, что именно нам не хватает. Нам нужно добавить случай для обработки ITriangle в нашу функцию площади.
function area(shape: Shape): number { switch (shape.type) { case 'Rectangle': return shape.width * shape.height case 'Circle': return Math.PI * Math.pow(shape.radius, 2) case 'Triangle': return (Math.sqrt(3)/4) * Math.pow(shape.side, 2) default: const msg: never = shape throw new TypeError(`Unknown type: ${msg}`) } }
Теперь все снова компилируется.
Заключение
Способность компилятора TypeScript различать набор типов на основе условных выражений - удивительно полезная функция, которая при правильном использовании значительно повысит надежность и читаемость вашего кода. Для выполнения этих проверок мы использовали следующие функции:
- Оператор типа «is», чтобы сообщить компилятору, что это за тип.
- Возможность использовать буквальные значения в качестве типов. Это относится к типам как к правилам, определяющим набор допустимых значений. В этом случае мы добавляем в набор буквальные значения.
- Способность TypeScript различать типы на основе уникальных свойств этих типов.
- Ключевое слово never для достижения полного соответствия типов.
Опять же, вместе они составляют набор функций TypeScript, который я больше всего ценю и который больше всего влияет на то, как я пишу код в повседневной жизни.
Еще статьи из этой серии
- Написание безопасного по типу (ish) кода JavaScript
- Строгие флаги компилятора
- Интерфейсы и структурная типизация
- Работа с типами и определениями типов
- Брендинг и тегирование
- Функциональные перегрузки