david
david5mo ago

How to treat mongoose/mongodb ObjectIDs as their serialized string values on the frontend?

Node Environment Version: v20.10.0 Package Manager: PNPM Workspaces Repo Setup: Monorepo/Turborepo Environment: WSL with VSCode TRPC Versions:
"@trpc/client": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
"@trpc/client": "11.0.0-rc.403",
"@trpc/react-query": "11.0.0-rc.403",
"@trpc/server": "11.0.0-rc.403",
If I have a query procedure as follows:
get_all_users: authenticatedProcedure.query(async ({ ctx }) => {
const users = await User.find({})
.select<{
user_name: string;
}>("user_name")
.lean();

return users;
}),
get_all_users: authenticatedProcedure.query(async ({ ctx }) => {
const users = await User.find({})
.select<{
user_name: string;
}>("user_name")
.lean();

return users;
}),
and a simple frontend render on react as follows:
import { trpc } from "@/features/trpc";

const ViewUsers = () => {
const { data, isLoading, error } = trpc.admin.get_all_users.useQuery();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
{data?.map((user) => <div key={user._id}>{user.user_name}</div>)}
</div>
);
};
export default ViewUsers;
import { trpc } from "@/features/trpc";

const ViewUsers = () => {
const { data, isLoading, error } = trpc.admin.get_all_users.useQuery();

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
{data?.map((user) => <div key={user._id}>{user.user_name}</div>)}
</div>
);
};
export default ViewUsers;
The frontend gives an error like:
Type 'ObjectId' is not assignable to type 'Key | null | undefined'.ts(2322)
index.d.ts(261, 9): The expected type comes from property 'key' which is declared here on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'
(property) React.Attributes.key?: React.Key | null | undefined
Type 'ObjectId' is not assignable to type 'Key | null | undefined'.ts(2322)
index.d.ts(261, 9): The expected type comes from property 'key' which is declared here on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'
(property) React.Attributes.key?: React.Key | null | undefined
However. The actual serialized value is of course a string for each _id. What is the current expectation to handle these values? Of course, you can manually say String(user._id) but is there a more appropriate method to handle this? Like a way to inform TS that these are strings and not ObjectId's on the frontend? I've tried my best to look at some other threads mentioning it, they however seem to mention declaring additional zod output schemas which feels quite cumbersome if you're intending on having varying different data returned. The goal is to have the values treated as strings i feel. Thanks for any help guys :)
2 Replies
david
davidOP4w ago
Hey @BenZ just saw you upvoted. Incase it's of use to you, apologies realised I hadn't updated my thread. A sort of non-perfect solution i created was a utility function that wraps the return statement in any query, it is based off of the supported superjson types, i havent had any issues with it so far, your mileage may vary though!
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ObjectId } from "bson";
import { Url } from "url";

// SuperJSON supported types
type SuperJSONSupportedTypes =
| Date
| Set<any>
| Map<any, any>
| RegExp
| undefined
| null
| bigint
| Url

type Jsonify<T> = T extends ObjectId
? string
: T extends SuperJSONSupportedTypes
? T
: T extends object
? { [K in keyof T]: Jsonify<T[K]> }
: T;

export const jsonify = <T>(data: T): Jsonify<T> => {
return data as unknown as Jsonify<T>;
};
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ObjectId } from "bson";
import { Url } from "url";

// SuperJSON supported types
type SuperJSONSupportedTypes =
| Date
| Set<any>
| Map<any, any>
| RegExp
| undefined
| null
| bigint
| Url

type Jsonify<T> = T extends ObjectId
? string
: T extends SuperJSONSupportedTypes
? T
: T extends object
? { [K in keyof T]: Jsonify<T[K]> }
: T;

export const jsonify = <T>(data: T): Jsonify<T> => {
return data as unknown as Jsonify<T>;
};
e.g
get_all_users: authenticatedProcedure.query(async ({ ctx }) => {
const users = await User.find({})
.select<{
user_name: string;
}>("user_name")
.lean();

return jsonify(users);
}),
get_all_users: authenticatedProcedure.query(async ({ ctx }) => {
const users = await User.find({})
.select<{
user_name: string;
}>("user_name")
.lean();

return jsonify(users);
}),
BenZ
BenZ4w ago
Thanks! Thats a similar solution to what I have now, but doing the type casting in the procedure return is much simpler. I was hoping to find a way to convert all ObjectIds to strings with a custom transformer, but have not had any luck. Think I will go with this for now! Thanks again.