Typescript: На самом деле он не проверяет ваши типы.

Typescript — хорошая вещь: он позволяет вам определять типы и следить за тем, чтобы ваши классы и функции соответствовали определенным ожиданиям. Он заставляет вас думать о том, какие данные вы помещаете в функцию и что вы получите из нее. Если вы ошибетесь и попытаетесь вызвать функцию, которая ожидает от вас, скажем, число, компилятор сообщит вам об этом. И это хорошо.

Иногда это приводит к заблуждению: Я встречал людей, которые считали, что typescript будет следить за тем, чтобы типы были такими, как вы говорите. Но я должен вам сказать: Typescript этого не делает.

Почему? Ну, Typescript работает на уровне компилятора, а не во время выполнения. Если вы посмотрите на то, как выглядит код, который производит Typescript, то увидите, что он переводит на Javascript и вычеркивает из кода все типы.

Код Typescript:

const justAFunction = (n: number): string => {
  return `${n}`
}

console.log(justAFunction)
Вход в полноэкранный режим Выход из полноэкранного режима

Результирующий код Javascript (при условии, что вы транслируете в более современную версию EcmaScript):

"use strict";
const justAFunction = (n) => {
    return `${n}`;
};
console.log(justAFunction);
Войти в полноэкранный режим Выйти из полноэкранного режима

Проверяется только правильность типов на основе вашего исходного кода. Он не проверяет фактические данные.

Проверка типов

Значит, typescript бесполезен? Нет, далеко нет. При правильном использовании он заставляет вас проверять типы, если нет никаких гарантий («к сожалению», он также предоставляет несколько легких путей выхода из ситуации).

Давайте немного изменим наш пример:

const justAFunction = (str: string[] | string): string => {
  return str.join(' ') 
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))
Вход в полноэкранный режим Выйти из полноэкранного режима

При компиляции это приведет к следующей ошибке:

index.ts:2:14 - error TS2339: Property 'join' does not exist on type 'string | string[]'.
  Property 'join' does not exist on type 'string'.

2   return str.join(' ')
               ~~~~


Found 1 error in index.ts:2
Enter fullscreen mode Выйти из полноэкранного режима

Компилятор заставляет задуматься о типе переменной str. Одним из решений было бы разрешить в функции только string[]. Другой — проверить, содержит ли переменная правильный тип.

const justAFunction = (str: string[] | string): string => {
  if (typeof str === 'string') {
    return str
  }

  return str.join(' ') 
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))
Вход в полноэкранный режим Выйти из полноэкранного режима

Это также можно перевести в Javascript и проверить тип. В этом случае у нас будет только гарантия, что это string, и мы будем только предполагать, что это массив.

Во многих случаях этого достаточно. Но как только нам приходится иметь дело с внешними источниками данных — такими как API, файлы JSON, пользовательский ввод и тому подобное — мы не должны считать, что данные верны. Мы должны проверить данные, и есть возможность убедиться в правильности типов.

Сопоставление внешних данных с вашими типами

Итак, первым шагом для решения этой проблемы, вероятно, будет создание фактических типов для отражения ваших данных.

Предположим, что API возвращает запись о пользователе следующим образом:

{
  "firstname": "John",
  "lastname": "Doe",
  "birthday": "1985-04-03"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Тогда мы, возможно, захотим создать интерфейс для этих данных:

interface User {
  firstname: string
  lastname: string
  birthday: string
}
Вход в полноэкранный режим Выход из полноэкранного режима

И использовать fetch для получения пользовательских данных из API:

const retrieveUser = async (): Promise<User> => {
  const resp = await fetch('/user/me')
  return resp.json()
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это будет работать, и typescript будет распознавать тип пользователя. Но он может обманывать вас. Допустим, день рождения будет содержать число с меткой времени (это может быть несколько проблематично для людей, родившихся до 1970 года… но сейчас не об этом). Тип будет по-прежнему рассматривать дату рождения как строку, несмотря на наличие в ней фактического числа… а Javascript будет рассматривать ее как число. Потому что, как мы уже говорили, Typescript не проверяет фактические значения.

Что мы должны сделать сейчас. Напишите функцию валидатора. Это может выглядеть примерно так:

const validate = (obj: any): obj is User => {
  return obj !== null 
    && typeof obj === 'object'
    && 'firstname' in obj
    && 'lastname' in obj
    && 'birthday' in obj
    && typeof obj.firstname === 'string'
    && typeof obj.lastname === 'string'
    && typeof obj.birthday === 'string'
}

const user = await retrieveUser()

if (!validate(user)) {
  throw Error("User data is invalid")
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Существуют протоколы, по своей сути работающие с типами: gRPC, tRPC, проверка JSON на соответствие схеме и GraphQL (в определенной степени). Они обычно очень специфичны для определенного случая использования. Нам может понадобиться более общий подход.

Вводим Zod

Zod — это недостающее звено между типами Typescript и принудительным применением типов в Javascript. Он позволяет одним движением определить схему, вывести тип и проверить данные.

Наш тип User будет определен следующим образом:

import { z } from 'zod'

const User = z.object({
    firstname: z.string(),
    lastname: z.string(),
    birthday: z.string()
  })
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем тип может быть извлечен (выведен) из этой схемы.

const UserType = z.infer<User>
Вход в полноэкранный режим Выход из полноэкранного режима

а валидация выглядит следующим образом

const userResp = await retrieveUser()
const user = User.parse(userResp)
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть тип и проверенные данные, а код, который нам пришлось написать, лишь незначительно больше, чем без функции проверки.

Заключение

При работе с Typescript важно знать разницу между проверками компилятора и проверкой во время выполнения. Чтобы убедиться, что внешние данные соответствуют нашим типам, нам необходимо иметь некоторую проверку. Zod — отличный инструмент для решения именно этой задачи без особых накладных расходов и гибким способом.

Спасибо за прочтение.

Оцените статью
devanswers.ru
Добавить комментарий