You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix Action类型推断结合Zod验证的TS错误排查

在Remix多Action场景下结合Zod类型推断的解决方案

问题根源

单Action场景下,useActionData<typeof action>能正确推断Zod格式化错误的类型,但多Action时返回类型是多个Zod错误结构的联合类型,TypeScript无法确定当前actionData对应哪个Action的返回,导致访问_errors或动态索引时出现类型报错。


可行解决方案

1. 给Action返回加类型标识(推荐)

在每个Action的返回结果中添加一个固定的actionType字段,通过该字段缩小TypeScript的类型判断范围。

服务端Action代码:

import { z } from "zod";
import { ActionFunctionArgs } from "@remix-run/node";

// 定义不同表单的Zod校验规则
const CreateSchema = z.object({
  some_string: z.string().min(1, "不能为空"),
});

const UpdateSchema = z.object({
  id: z.string().uuid("无效ID"),
  another_field: z.number().int("必须是整数"),
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const actionType = formData.get("actionType") as "create" | "update";

  // 创建动作
  if (actionType === "create") {
    const result = CreateSchema.safeParse(Object.fromEntries(formData));
    if (!result.success) {
      return {
        actionType: "create" as const, // 固定类型,用于组件侧类型缩小
        fields: Object.fromEntries(formData),
        errors: result.error.format(),
      };
    }
    // 执行创建逻辑...
  }

  // 更新动作
  if (actionType === "update") {
    const result = UpdateSchema.safeParse(Object.fromEntries(formData));
    if (!result.success) {
      return {
        actionType: "update" as const,
        fields: Object.fromEntries(formData),
        errors: result.error.format(),
      };
    }
    // 执行更新逻辑...
  }

  return { actionType: "unknown" as const };
}

组件侧代码:

import { useActionData } from "@remix-run/react";
import type { action } from "./route";

const actionData = useActionData<typeof action>();

// 根据actionType缩小类型,解决_errors访问报错
function getFieldError(input: keyof typeof CreateSchema["shape"] | keyof typeof UpdateSchema["shape"]) {
  if (actionData?.actionType === "create") {
    return actionData.errors?.[input as keyof typeof actionData.errors]?._errors?.[0];
  }
  if (actionData?.actionType === "update") {
    return actionData.errors?.[input as keyof typeof actionData.errors]?._errors?.[0];
  }
  return undefined;
}

2. 手动定义联合类型并通过字段存在性缩小范围

如果不想加actionType,可以手动定义所有Action返回的类型联合,通过检查错误对象的字段存在性来缩小类型:

import { z } from "zod";
import { useActionData } from "@remix-run/react";

const CreateSchema = z.object({ some_string: z.string() });
const UpdateSchema = z.object({ id: z.string(), another_field: z.number() });

// 手动定义每个Action的错误返回类型
type CreateActionError = {
  fields: Record<string, FormDataEntryValue>;
  errors: z.ZodFormattedError<z.infer<typeof CreateSchema>, string>;
};

type UpdateActionError = {
  fields: Record<string, FormDataEntryValue>;
  errors: z.ZodFormattedError<z.infer<typeof UpdateSchema>, string>;
};

type ActionData = CreateActionError | UpdateActionError | undefined;

const actionData = useActionData() as ActionData;

// 通过检查字段存在性缩小类型
function getCreateError() {
  if (actionData && "some_string" in actionData.errors) {
    // TypeScript此时知道这是CreateActionError类型
    return actionData.errors.some_string._errors[0];
  }
  return undefined;
}

3. 解决动态索引报错

如果需要用动态字符串作为索引,可通过泛型函数约束或类型断言解决:

  • 泛型函数方式(更安全):
function getError<T extends keyof (typeof actionData)["errors"]>(input: T) {
  return actionData?.errors?.[input]?._errors?.[0];
}

// 使用时传入具体字段名,自动获得类型提示
const error = getError("some_string");
  • 类型断言方式(快速解决):
const input = "some_string";
const error = actionData?.errors?.[input as keyof typeof actionData.errors]?._errors?.[0];

内容的提问来源于stack exchange,提问作者sthomas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 15:05:34