17. TypeScript в Remix
Remix создан с первоклассной поддержкой TypeScript. Типизация loader и action данных происходит автоматически через typeof loader.
Базовая типизация
Заголовок раздела «Базовая типизация»// Типы автоматически выводятся из loaderexport async function loader({ params }: LoaderFunctionArgs) { const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) throw new Response("Not Found", { status: 404 });
return { post }; // Нет json() — в Remix v2 возвращаем напрямую}
export default function Post() { // post: { id: string, title: string, content: string, ... } const { post } = useLoaderData<typeof loader>(); return <h1>{post.title}</h1>;}LoaderFunctionArgs и ActionFunctionArgs
Заголовок раздела «LoaderFunctionArgs и ActionFunctionArgs»import type { LoaderFunctionArgs, ActionFunctionArgs, MetaFunction, LinksFunction,} from "@remix-run/node";
export async function loader({ request, params, context }: LoaderFunctionArgs) { // request: Request // params: Record<string, string | undefined> // context: AppLoadContext}
export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const email = formData.get("email"); // → string | File | null // Кастуем после валидации: const validEmail = email as string;}Типизация useActionData
Заголовок раздела «Типизация useActionData»type ActionData = { errors?: { email?: string; password?: string; }; user?: { id: string; email: string; };};
export async function action({ request }: ActionFunctionArgs) { // ...}
export default function LoginForm() { const actionData = useActionData<ActionData>(); // actionData: ActionData | undefined actionData?.errors?.email; // string | undefined}useRouteLoaderData — данные родительских маршрутов
Заголовок раздела «useRouteLoaderData — данные родительских маршрутов»import type { loader as rootLoader } from "~/root";import { useRouteLoaderData } from "@remix-run/react";
export default function Child() { const rootData = useRouteLoaderData<typeof rootLoader>("root"); // rootData полностью типизирован! return <p>{rootData?.user.name}</p>;}Zod для валидации форм
Заголовок раздела «Zod для валидации форм»import { z } from "zod";
const RegisterSchema = z.object({ email: z.string().email("Неверный email"), password: z.string().min(8, "Минимум 8 символов"), name: z.string().min(2, "Минимум 2 символа"),});
export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const raw = Object.fromEntries(formData);
const result = RegisterSchema.safeParse(raw);
if (!result.success) { return json({ errors: result.error.flatten().fieldErrors, }, { status: 400 }); }
// result.data полностью типизирован и валиден const { email, password, name } = result.data; await createUser({ email, password, name }); return redirect("/dashboard");}Типизация параметров маршрута
Заголовок раздела «Типизация параметров маршрута»// Кастомный хук для типизированных параметровfunction useTypedParams<T extends Record<string, string>>() { const params = useParams(); return params as T;}
// Использованиеconst { id, slug } = useTypedParams<{ id: string; slug: string }>();