remix: V2 Bug: New `JsonifyObject` type causing union type issues with Typescript

What version of Remix are you using?

2.0.1

Are all your remix dependencies & dev-dependencies using the same version?

  • Yes

Steps to Reproduce

If you have a loader or action function which can return different object structures the new JsonifyObject abstraction type appears to not be properly understood by typescript meaning the code below cannot compile let alone run, even though it was valid in 1.19.* - and theoretically should be valid code.

app/routes/test.tsx

import type { ActionFunctionArgs } from "@remix-run/node";
import { useActionData } from "@remix-run/react";
import { json } from "@remix-run/node";

export async function action (args: ActionFunctionArgs) {
	const formData = await args.request.formData();
	const action = formData.get("action")?.toString() || "";

	switch (action) {
		case "a": return json({a: true});
		case "b": return json({b: true});
		default: throw new Response("Unexpected form action", {
			statusText: "Bad request",
			status: 400
		});
	}
};

export default function Index() {
	const actionData = useActionData<typeof action>();
	if (actionData?.a) console.log("It was a");
	if (actionData?.b) console.log("It was B");

	return <div>Hi</div>;
}

Expected Behavior

Typescript should correctly identify the type as effectively

const actionData: { a: boolean } | { b: boolean } | undefined;

And it indeeed identifying the type to be something close to this:

const actionData: JsonifyObject<{
    a: boolean;
}> | JsonifyObject<{
    b: boolean;
}> | undefined

Actual Behavior

Line 20: Property 'a' does not exist on type 'JsonifyObject<{ a: boolean; }> | JsonifyObject<{ b: boolean; }>'.
  Property 'a' does not exist on type 'JsonifyObject<{ b: boolean; }>'.ts(2339)

Line 21: Property 'b' does not exist on type 'JsonifyObject<{ a: boolean; }> | JsonifyObject<{ b: boolean; }>'.
  Property 'b' does not exist on type 'JsonifyObject<{ a: boolean; }>'.ts(2339

About this issue

  • Original URL
  • State: closed
  • Created 9 months ago
  • Reactions: 5
  • Comments: 17 (4 by maintainers)

Most upvoted comments

Also the update to have Jsonify take over as the type of all child objects of a Jsonify object is quite annoying. Because it then means any child object is defined as possibly being null, when actually it’s often not.

i.e. here dictionaryRef is actually a 1:m include from prisma, so they’re not null… image
image
image
.dictionary. clearly should not be null

I agree that it’s probably a Typescript weirdness issue, but this worked before 2.0, and then updating to 2.0 broke all of my type-checking, and this wasn’t even listed as a potential pain point like the changes to file routing, meta functions and the like.

I feel like this change really should have been outlined in the upgrading to v2

Reading #7246 it looks like there is a patch in the works which changes the type information to Typescript can infer the union type correct, am I understanding this correctly? If so I’ll live with the pain of any everywhere until the patch is finally mainstream.

Interestingly there is no error during the npm run build process, not when actually loading the page,

esbuild understands TypeScript syntax, but does not do typechecking.

BTW: I wrote remix-typedjson precisely because of this issue. My package includes metadata about the types so your userLoaderData gets the actual TS type, and not the JsonifyObject wrapper type.

If I switch to TS 5.0.4 from 5.2.2, then issues go away… This is a TS issue then… 🫡 Thank you for your time

@alexanderMontague here’s a reproduction of issue without any Remix code. Its a consequence of how Typescript works and is not something fixable by Remix.

For more in depth explanation of TS behavior see: https://twitter.com/pcattori/status/1598359344827056131

This is how Typescript works with non-discriminated unions, and not a Remix bug.

If you want to narrow the type, you’ll need to use a discriminated field in your union