open atlas
↑ К треку
Система типов TypeScript вглубь TS · 08 · 03

tRPC: сквозные типы без кодогенерации и чего это стоит

tRPC даёт сквозную типобезопасность без кодогена: клиент выводит весь тип API из `typeof appRouter`, и типы текут через провод на этапе компиляции. «Без кодогена» значит, что клиент и сервер делят тип (`import type`); цена — время вывода и лаги редактора.

TS Senior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Вы переименовываете серверную процедуру с getUser на getUserById и добавляете аргумент includeDeleted: boolean. Жмёте «сохранить». Ещё не трогая клиент, три места вызова в двух фронтенд-файлах загораются красным: Property 'getUser' does not exist, а единственное место, что её вызывало, теперь без нового аргумента. Кодогенерация не запускалась. OpenAPI-файл не перегенерировался. Никакого npm run gen. Клиент просто знал — потому что тип API клиента не отдельная копия, а вывод из типа роутера сервера, пересчитываемый на каждом нажатии клавиши. Это tRPC, и вся магия — система типов, которую вы уже изучили.

Весь трюк: typeof appRouter

На сервере вы строите роутер. Его тип — не значение — кодирует каждое имя процедуры, вход и выход. Вы экспортируете этот тип и отдаёте клиенту как дженерик:

// server/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();

export const appRouter = t.router({
  getUserById: t.procedure
    .input(z.object({ id: z.number(), includeDeleted: z.boolean() }))
    .query(({ input }) => findUser(input.id)), // возвращает User
});

export type AppRouter = typeof appRouter; // ^? тип всего роутера
// client/trpc.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../server/router"; // только тип — см. ниже

const trpc = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: "/trpc" })] });

const user = await trpc.getUserById.query({ id: 42, includeDeleted: false });
// ^? User  — выведено сквозь из типа возврата сервера
// trpc.getUser  // Error: Property 'getUser' does not exist

createTRPCClient<AppRouter> берёт тип роутера сервера и через большой внутренний mapped/conditional-тип превращает его в интерфейс вызовов клиента: процедура getUserById.query роутера appRouter становится trpc.getUserById.query(input): Promise<output>. Тип входа берётся из z.infer вашей zod-схемы; тип выхода — из типа возврата резолвера. Оба текут к клиенту без единого сгенерированного файла. (В реальном приложении вы редко вызываете .query напрямую — вы прогоняете его через клиентский кэш вроде React Query, где эти выведенные типы доходят до ваших хуков; см. трек Frontend про клиентские кэши.)

Почему это работает

Тяжёлую работу здесь делает typeof в пространстве типов (оператор из раздела 04). typeof appRouter поднимает рантайм-значение (объект роутера) в тип, который клиент может потребить. Клиент никогда не импортирует рантайм роутера — только его тип — так что обращения сервера к БД, секреты и зависимости не попадают в бандл браузера. Контракт — это тип; реализация остаётся дома.

«Без кодогена» — что это реально значит и жёсткие пределы

Прежде чем брать tRPC в следующий проект, спросите себя: может ли клиент всегда сделать import type с сервера? Если да — живой вывод достаётся бесплатно. Если нет — нужен контракт провода, и это меняет всё.

REST + OpenAPI генерирует файл client.ts из спеки; GraphQL (язык запросов с типизированной схемой) генерирует типы из схемы. tRPC не генерирует ничего. Размен в том, что клиент обязан мочь сделать import type серверного AppRouter — а это значит:

  • Одна сборка TypeScript / монорепо. Клиент и сервер компилируются вместе (или сервер поставляет .d.ts, который потребляет клиент). Формата провода, описывающего API, нет; тип — это контракт, а тип может пересечь только границу компиляции, не границу сети или языка.
  • TypeScript на обоих концах. Swift- или Kotlin-клиент не может потребить TS-тип. Для полиглот- или сторонних потребителей нужен настоящий контракт провода — OpenAPI или GraphQL — и именно тогда их кодогенерация окупается.

Цена: время вывода роутера и лаги редактора

За удобство платят работой компилятора. Тип клиента — функция от всего типа роутера сервера, пересчитываемая tsc и языковым сервисом редактора. На роутере с сотнями процедур, глубоко вложенными саброутерами и сложными zod-схемами вы это чувствуете: всплывающие подсказки крутятся, автодополнение лагает, прогоны tsc ползут. Вывод не кэшируется так, как кэшировался бы сгенерированный файл — файла нет. Смягчения:

  • Разбивайте на саброутеры и избегайте одного монолитного роутера на 500 процедур; меньшие единицы перевыводятся быстрее.
  • Аннотируйте типы возврата процедур явно, чтобы тип выхода резолвера не выводился каждый раз сквозь весь слой данных.
  • Держите zod-схемы разумными — гигантский z.discriminatedUnion на 40 членов дорог в выводе везде, где используется.
  • Тянитесь к кодогену, когда вывод перестаёт масштабироваться: это настоящая точка, где модель OpenAPI/GraphQL «сгенерировал раз — прочитал файл» выигрывает по производительности компилятора, разменивая живой вывод на шаг сборки.

Вместе эти смягчения сокращают перевыводимую поверхность; без них монолитный роутер на 500 процедур заставляет весь языковой сервис ползти на каждом нажатии клавиши.

Главный самострел: случайный импорт серверного рантайма

Поскольку сервер и клиент живут в одном репо, легко написать import { appRouter } (значение) вместо import type { AppRouter } (тип). Импорт значения тащит весь серверный рантайм — драйверы БД, секреты, Node-only-модули — в бандл клиента и даже может отправить учётные данные в браузер. Всегда import type для роутера на клиенте. Включите "verbatimModuleSyntax": true (раздел 07), чтобы случайный импорт-значением типа был ошибкой компиляции, а не тихим раздуванием бандла.

Викторина

tRPC даёт «сквозную типобезопасность без кодогена». Что «без кодогена» требует взамен?

Расставь шаги по порядку

Расставьте, как типы серверной процедуры доходят до типизированного места вызова клиента в tRPC:

  1. 1 Сервер строит appRouter; каждая процедура несёт zod-схему входа и тип возврата резолвера
  2. 2 Экспортировать ТИП роутера: type AppRouter = typeof appRouter
  3. 3 Клиент импортирует его через `import type` и передаёт: createTRPCClient<AppRouter>
  4. 4 mapped/conditional-тип превращает тип роутера в интерфейс клиента, выводя каждый вход/выход
  5. 5 Место вызова trpc.x.query(input) полностью типизировано; переименование серверной процедуры ломает клиент на этапе компиляции
Вспомните перед уходом
  1. 01
    Механически: как клиент получает типы API в tRPC без кодогена?
  2. 02
    Что «без кодогена» вынуждает взамен и когда лучше предпочесть кодоген OpenAPI/GraphQL?
  3. 03
    Почему гигантский tRPC-роутер замедляет редактор и в чём самострел импорта рантайма?
Итог

Теперь вы понимаете движок за заявлением tRPC «без кодогена»: typeof appRouter поднимает роутер в тип, клиент выводит из него весь свой API на этапе компиляции, цена — связанность через общий тип плюс стоимость вывода, а самострел — импорт значением, утекающий сервер в браузер. Вы видели три точки на спектре граничной типизации — контракты, смоделированные руками, и Result-типы; рантайм-валидация zod; и вывод tRPC на этапе компиляции. Этот капстоун потребления типизированных систем готовит обратный навык: в следующем уроке вы будете проектировать generic-API библиотек, которые так же красиво выводятся для ваших вызывающих, и всё сходится в капстоуне типизированной фичи в разделе 09. Теперь, когда встретите на клиенте import { appRouter } в tRPC-проекте, вы знаете: надо менять на import type { AppRouter } — иначе секреты сервера окажутся в бандле браузера.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.