useActionData()に渡すtypeof actionが期待通りでない場合

Remixで`action`を実装するとき、必ず`useActionData()`フックを使ってフォームのバリデーションを行うと思います。

```tsx
export async function action({ request }: ActionArgs) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const redirectTo = safeRedirect(formData.get("redirectTo"), "/");

  if (!validateEmail(email)) {
    return json(
      { errors: { email: "Email is invalid", password: null } },
      { status: 400 }
    );
  }

  if (typeof password !== "string" || password.length === 0) {
    return json(
      { errors: { email: null, password: "Password is required" } },
      { status: 400 }
    );
  }

  if (password.length < 8) {
    return json(
      { errors: { email: null, password: "Password is too short" } },
      { status: 400 }
    );
  }

  const existingUser = await getUserByEmail(email);
  if (existingUser) {
    return json(
      {
        errors: {
          email: "A user already exists with this email",
          password: null,
        },
      },
      { status: 400 }
    );
  }

  const user = await createUser(email, password);

  return createUserSession({
    request,
    userId: user.id,
    remember: false,
    redirectTo,
  });
}

export default function Join() {
  const actionData = useActionData<typeof action>();
  const emailRef = React.useRef<HTMLInputElement>(null);
  const passwordRef = React.useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (actionData?.errors?.email) {
      emailRef.current?.focus();
    } else if (actionData?.errors?.password) {
      passwordRef.current?.focus();
    }
  }, [actionData]);

  return (
    ...
  )
}
```

このとき、`action()`の返り値として`remix-auth`で提供される`authenticate()`の結果を返すことを考えてみましょう。

```tsx
export async function action({ request }: ActionArgs) {
  ...
  const newRequest = request.clone();
  ...
  if (!validateEmail(email)) {
    return json(
      { errors: { email: "Email is invalid", password: null } },
      { status: 400 }
    );
  }
  ...
  return await authenticator.authenticate("user-pass", newRequest, {
    successRedirect: redirectTo,
    failureRedirect: "/",
  });
}
```

以前のように`typeof action`を`useActionData()`に渡してしまうと`actionData?.errors`を参照しようとするときエラーが発生してしまいます。

```shell
Property 'errors' does not exist on type 'SerializeObject<UndefinedToOptional<User>> | SerializeObject<UndefinedToOptional<TypedResponse<{ errors: { email: string; password: null; }; }>>> | SerializeObject<...>'.
  Property 'errors' does not exist on type 'SerializeObject<UndefinedToOptional<User>>'.ts(2339)
```

ご存知の通り、`authenticate()`にはオプションとして`successRedirect`と`failureRedirect`が渡されています。なので、実際に`errors`を参照する必要がある場合には、その前の`if`ブロックで返し終わっているため、`authenticate()`に到達する頃にはリダイレクトが発生しています。
つまり、実際に`User`が返ってくることはないのです。

そこで、`typeof action`で返される関数の型の返り値の型から、`User`を除外してあげましょう。

```ts
type ActionType = (args: Parameters<typeof action>) => Promise<Exclude<Awaited<ReturnType<typeof action>>, User>>;
```

これで、`useActionData<ActionType>()`とすることで、`actionData?.errors`を参照することができるようになります。

```tsx
const actionData = useActionData<ActionType>();

React.useEffect(() => {
    if (actionData?.errors?.email) {
      ...
    } else if (actionData?.errors?.password) {
      ...
    }
  }, [actionData]);
```

Remix Stacksのテンプレートで用意されている内容に、`remix-auth`を追加した場合にこのような問題によく遭遇すると思うので、みなさんも参考にしてみてください。