Type guards

Type guards — это механизм в TypeScript, который позволяет точно определить тип переменной в определённом контексте кода. В фреймворке Fresh, построенном на Deno и поддерживающем TypeScript из коробки, использование type guards особенно полезно при работе с данными из API, форм, параметров маршрутов и состояния компонентов.


Основы Type Guards

Type guard — это выражение, которое сужает тип переменной внутри блока кода. Это необходимо, когда переменная имеет объединённый тип (union type) или тип any, и нужно гарантировать безопасность операций с ней.

Пример базового type guard:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

const input: unknown = "Fresh";

if (isString(input)) {
  console.log(input.toUpperCase()); // Тип безопасно сузился до string
}

Ключевой момент: конструкция value is string в определении функции позволяет TypeScript понимать, что после проверки переменная имеет конкретный тип.


Type Guards в контексте Fresh

Fresh активно использует функции обработки запросов и компонентный рендеринг, где данные приходят в виде параметров props или Request. Type guards помогают работать с ними безопасно.

Пример: безопасная работа с параметрами запроса

import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  GET(req, ctx) {
    const url = new URL(req.url);
    const idParam = url.searchParams.get("id");

    if (isString(idParam)) {
      return new Response(`ID: ${idParam}`);
    } else {
      return new Response("ID не указан", { status: 400 });
    }
  },
};

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Здесь type guard гарантирует, что переменная idParam действительно строка, прежде чем выполнять с ней операции.


Встроенные Type Guards TypeScript

TypeScript предоставляет несколько встроенных возможностей для создания type guards без написания функций:

  1. typeof Проверяет примитивные типы: string, number, boolean, symbol, bigint, undefined, function.

    const value: string | number = 42;
    
    if (typeof value === "number") {
      console.log(value + 1); // безопасно
    }
  2. instanceof Проверяет, является ли объект экземпляром класса или конструктора.

    class User {
      constructor(public name: string) {}
    }
    
    const maybeUser: User | null = new User("Alice");
    
    if (maybeUser instanceof User) {
      console.log(maybeUser.name);
    }
  3. in Проверяет наличие свойства в объекте. Особенно полезно для discriminated unions.

    type Admin = { role: "admin"; privileges: string[] };
    type Guest = { role: "guest"; visitingSince: Date };
    
    function printInfo(user: Admin | Guest) {
      if ("privileges" in user) {
        console.log(user.privileges.join(", "));
      } else {
        console.log(user.visitingSince.toDateString());
      }
    }

Discriminated Unions и Type Guards

Discriminated unions — это объединения типов с общим “дискриминатором”, который используется для безопасного сужения типа. Fresh часто работает с такими структурами при обработке состояния компонентов.

type SuccessResponse = { status: "success"; dat a: string[] };
type ErrorResponse = { status: "error"; message: string };

function handleResponse(resp: SuccessResponse | ErrorResponse) {
  if (resp.status === "success") {
    console.log(resp.data.join(", "));
  } else {
    console.error(resp.message);
  }
}

Здесь resp.status является дискриминатором, а проверка resp.status === "success" служит type guard.


Пользовательские Type Guards

В Fresh, при работе с динамическими данными, часто создаются кастомные type guards:

interface Product {
  id: number;
  name: string;
  price: number;
}

function isProduct(obj: unknown): obj is Product {
  return typeof obj === "object" &&
         obj !== null &&
         "id" in obj &&
         "name" in obj &&
         "price" in obj;
}

const data: unknown = fetch("/api/product").then(res => res.json());

data.then(item => {
  if (isProduct(item)) {
    console.log(item.name, item.price);
  }
});

Такой подход предотвращает ошибки при работе с внешними API и динамическими объектами.


Type Guards и JSX в Fresh

При рендеринге компонентов можно использовать type guards для различения типов данных в props:

interface TextBlock {
  type: "text";
  content: string;
}

interface ImageBlock {
  type: "image";
  src: string;
}

type Block = TextBlock | ImageBlock;

export default function BlockRenderer({ block }: { block: Block }) {
  if (block.type === "text") {
    return <p>{block.content}</p>;
  } else {
    return <img src={block.src} alt="" />;
  }
}

Такой подход позволяет безопасно использовать свойства каждого варианта union, избегая ошибок времени выполнения.


Советы по применению

  • Использовать type guards при работе с внешними данными или API.
  • Применять дискриминаторы (type, kind, status) для union типов.
  • Создавать собственные функции-проверки для сложных структур.
  • Совмещать встроенные операторы (typeof, instanceof, in) с кастомными проверками для максимальной безопасности.

Type guards являются мощным инструментом, который позволяет совмещать динамику JavaScript с безопасностью TypeScript. В Fresh их применение обеспечивает стабильность и предсказуемость работы с props, API и состоянием компонентов, что особенно важно при построении масштабируемых приложений.