Remix TypeScript集成 #

一、TypeScript配置 #

1.1 tsconfig.json #

json
{
  "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "types": ["@remix-run/node", "vite/client"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ES2022",
    "strict": true,
    "noEmit": true,
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}

1.2 环境类型定义 #

创建 env.d.ts

tsx
/// <reference types="@remix-run/dev/vite" />
/// <reference types="@remix-run/node" />

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      SESSION_SECRET: string;
      NODE_ENV: "development" | "production" | "test";
    }
  }
  
  interface Window {
    ENV: {
      PUBLIC_API_URL: string;
    };
  }
}

export {};

二、Loader类型 #

2.1 基本类型 #

tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

export async function loader({ params, request }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  return json({ post });
}

2.2 类型推断 #

tsx
import { useLoaderData } from "@remix-run/react";

export default function Post() {
  // 自动推断类型
  const { post } = useLoaderData<typeof loader>();
  
  return <div>{post.title}</div>;
}

2.3 定义返回类型 #

tsx
interface LoaderData {
  post: Post;
  comments: Comment[];
}

export async function loader({ params }: LoaderFunctionArgs) {
  const [post, comments] = await Promise.all([
    getPost(params.id),
    getComments(params.id),
  ]);
  
  return json<LoaderData>({ post, comments });
}

三、Action类型 #

3.1 基本类型 #

tsx
import type { ActionFunctionArgs } from "@remix-run/node";

export async function action({ request, params }: ActionFunctionArgs) {
  const formData = await request.formData();
  // ...
}

3.2 表单数据类型 #

tsx
import { z } from "zod";

const postSchema = z.object({
  title: z.string().min(3),
  content: z.string().min(10),
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const result = postSchema.safeParse(Object.fromEntries(formData));
  
  if (!result.success) {
    return json({ errors: result.error.flatten() }, { status: 400 });
  }
  
  const post = await createPost(result.data);
  return redirect(`/posts/${post.id}`);
}

四、组件类型 #

4.1 路由组件 #

tsx
import type { MetaFunction, LinksFunction } from "@remix-run/node";
import { useLoaderData, useActionData } from "@remix-run/react";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title },
    { name: "description", content: data?.post.excerpt },
  ];
};

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesUrl },
];

export default function Post() {
  const { post } = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

4.2 普通组件 #

tsx
interface ButtonProps {
  variant?: "primary" | "secondary";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}

export function Button({
  variant = "primary",
  size = "md",
  disabled,
  onClick,
  children,
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

五、API类型 #

5.1 API响应类型 #

tsx
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  
  return json<ApiResponse<Post>>({
    success: true,
    data: post,
  });
}

5.2 fetcher类型 #

tsx
import { useFetcher } from "@remix-run/react";

export function useApiFetcher<T>(action: string) {
  const fetcher = useFetcher<ApiResponse<T>>();
  
  return {
    submit: (data: Record<string, any>) => {
      fetcher.submit(data, { method: "post", action });
    },
    data: fetcher.data?.data,
    error: fetcher.data?.error,
    isLoading: fetcher.state !== "idle",
  };
}

六、类型工具 #

6.1 路由类型 #

tsx
import type { RouteMatch } from "@remix-run/react";

export function useRouteData<T>(routeId: string): T | undefined {
  const matches = useMatches();
  const match = matches.find((m) => m.id === routeId);
  return match?.data as T | undefined;
}

6.2 参数类型 #

tsx
type RouteParams<T extends string> = T extends `${string}$${infer Param}`
  ? { [K in Param]: string }
  : {};

export async function loader<Params extends string>({
  params,
}: LoaderFunctionArgs & { params: RouteParams<Params> }) {
  // params 有类型推断
}

七、最佳实践 #

7.1 严格模式 #

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

7.2 类型导出 #

tsx
export type { User, CreateUserInput };
export { getUser, createUser };

7.3 类型复用 #

tsx
export type { LoaderData as PostLoaderData } from "./routes/posts.$id";
export type { LoaderData as UserLoaderData } from "./routes/users.$id";

八、总结 #

本章我们学习了:

  1. TypeScript配置:tsconfig.json设置
  2. Loader类型:类型推断和定义
  3. Action类型:表单数据类型
  4. 组件类型:路由和普通组件
  5. 最佳实践:严格模式和类型复用

核心要点:

  • 使用 typeof loader 推断类型
  • 定义清晰的接口类型
  • 启用严格模式
最后更新:2026-03-28