Bence (Arzen) - Hey guys 🙂I have a (might be)...

Hey guys 🙂 I have a (might be) silly question. I have a schema exported as module:
// schema.ts
export const mySchema = z.string()
// schema.ts
export const mySchema = z.string()
It is being imported and used in another schema like so:
import { mySchema } from './schema'

const formInputs = z.object({
name: mySchema,
// some other fields
})
import { mySchema } from './schema'

const formInputs = z.object({
name: mySchema,
// some other fields
})
I am using react-hook-form with zodResolver and the validation error for the name field is the following: Required Keep it in mind, that mySchema in the example above is being used at many places but the form field name could be different. Is there a way I could customise the error message on the imported schema? something like:
const formInputs = z.object({
name: mySchema.doSomeMagic('Name is required'),
// some other fields
})
const formInputs = z.object({
name: mySchema.doSomeMagic('Name is required'),
// some other fields
})
11 Replies
Scott Trinh
Scott Trinh2mo ago
good question. is it truly something as simple as a z.string() object or is it more complicated than that normally? The typical way to resolve this is to add an error map, but if you're returning a schema object, I don't think there is a way to add an error map once the schema has been created.
Svish
Svish2mo ago
You can with a global error map, but only if the schema doesn't specify error messages explicitly. I think... maybe you can override it as well, not sure.
Scott Trinh
Scott Trinh2mo ago
well, i think the use case here is overriding it when it's used, rather than globally.
Scott Trinh
Scott Trinh2mo ago
Yeah you can add it when you make a schema, but not to a schema object that has already been created (string vs string()) We need a clone method You might be able to refactor the export to export a factory rather than an instance though. Depends on how arbitrarily complex these schemas are 😅 Like if it's just string export that schema constructor instead of an instance, you know? But I suspect these are instances with complex validations on them
Svish
Svish2mo ago
Probably. But yeah, it seems you can pass an error map when you use the schema, passing it to the .parse function. So maybe that's a middleground between using a global error map, and not being able to pass it to already created schemas 🤷‍♂️
Bence (Arzen)
Bence (Arzen)2mo ago
@here sorry for the late reply. I didn't get any notification from the server for some reason. The schema I am trying to override the error on is indeed simple. I'll get you guys some code example how it's being created then how I try to use it later in our codebase. Schema creation example:
function createOpaqueInstance<OpaqueType extends Newtype<unknown, unknown>>(
schema: z.ZodTypeAny,
isValid: (value: OpaqueType['_A']) => boolean,
) {
const _iso = iso<OpaqueType>()
const _prism = prism<OpaqueType>(isValid)
const _schema = schema.refine(isValid).transform(_iso.wrap)

const instance: NewTypeInstance<OpaqueType['_A'], OpaqueType> = {
prism: _prism,
schema: _schema,
unwrap: _iso.unwrap,
}

return instance
}
function createOpaqueInstance<OpaqueType extends Newtype<unknown, unknown>>(
schema: z.ZodTypeAny,
isValid: (value: OpaqueType['_A']) => boolean,
) {
const _iso = iso<OpaqueType>()
const _prism = prism<OpaqueType>(isValid)
const _schema = schema.refine(isValid).transform(_iso.wrap)

const instance: NewTypeInstance<OpaqueType['_A'], OpaqueType> = {
prism: _prism,
schema: _schema,
unwrap: _iso.unwrap,
}

return instance
}
I am using a couple of helper libraries which aren't really important to include here.
function isValidId(value: number | string): boolean {
// etc...
}

export function createIdInstance<OpaqueType extends Newtype<unknown, number>>() {
return createOpaqueInstance<OpaqueType>(z.string().or(z.number()), isValidId)
}
function isValidId(value: number | string): boolean {
// etc...
}

export function createIdInstance<OpaqueType extends Newtype<unknown, number>>() {
return createOpaqueInstance<OpaqueType>(z.string().or(z.number()), isValidId)
}
Then the way I create these types is the following:
export type UserId = Newtype<{ readonly UserId: unique symbol }, number>

export const userIdInstance = createIdInstance<UserId>()
export type UserId = Newtype<{ readonly UserId: unique symbol }, number>

export const userIdInstance = createIdInstance<UserId>()
Once I have the userIdInstance I can access the schema on it and safely wrap or unwrap any values.
userIdInstance.schema // gets back the schema which is just a z.string().or(z.number())
userIdInstance.schema // gets back the schema which is just a z.string().or(z.number())
The type we end up with is the following:
type A = z.infer<typeof userIdInstance.schema>

type A = Newtype<{
readonly UserId: unique symbol;
}, number>
type A = z.infer<typeof userIdInstance.schema>

type A = Newtype<{
readonly UserId: unique symbol;
}, number>
The way I use it is the following:
export const updateUserNameSchema = z.object({
userId: userIdInstance.schema, // I'd like to override the error message here somehow
name: z
.string({ required_error: "Name is required" })
.min(1, "Name is required")
})
export const updateUserNameSchema = z.object({
userId: userIdInstance.schema, // I'd like to override the error message here somehow
name: z
.string({ required_error: "Name is required" })
.min(1, "Name is required")
})
I don't think the global error map will work. I also tried to use refine and superRefine but if the example schema above (userIdSchema) failes, it will not propogate anything to the refine/superRefine block (as it is expected I believe)
Svish
Svish2mo ago
Well, not sure what to say here. Personally this is one of the reasons why I shy away from too much abstraction and "helpers" in these cases, and why I lean heavily on generic error messages from a global error map instead of specifying them directly in our schemas. Less issues with types, less need for picking and adjusting existing schemas, and so on. In your case, I'd consider just avoid the reuse and abstractions and just do:
export const updateUserNameSchema = z.object({
userId: z.number(),
name: z
.string()
.min(1)
})
export const updateUserNameSchema = z.object({
userId: z.number(),
name: z
.string()
.min(1)
})
Bence (Arzen)
Bence (Arzen)2mo ago
I had a sort of "solution" but thought there is something better that I do not know of:
const schema = z.object({
userId: z
.string({ required_error: 'User Id required') })
.min(1, 'User Id required')
.pipe(userIdInstance.schema)
})
const schema = z.object({
userId: z
.string({ required_error: 'User Id required') })
.min(1, 'User Id required')
.pipe(userIdInstance.schema)
})
I understand your point of view. My main reason why I want to enforce type safety and create reference and id types is to avoid passing in something unintended around my codebase and scratch my head why everything collapses. My issues it type aliases in TS. Consider the following code:
type UserId = number
type CustomerId = number

async function getCustomerData(customerId: CustomerId): Promise<object> {
// etc...
}

const userId: UserId = 123
const customerId: CustomerId = 456

// TS will allow this as the underlying type equivalent...
getCustomerData(userId)
type UserId = number
type CustomerId = number

async function getCustomerData(customerId: CustomerId): Promise<object> {
// etc...
}

const userId: UserId = 123
const customerId: CustomerId = 456

// TS will allow this as the underlying type equivalent...
getCustomerData(userId)
I "understand" that we only created a type alias but coming from Scala I hate this behaviour 😂
Svish
Svish2mo ago
Yeah... I highly recommend that you try to leave your nominal typing hangups in Scala, and instead embrace and get used to structural typing in js/ts. It has its downsides, but also several upsides. A main source of terrible js/ts code is from backend people refusing to change their ways, making classes and interfaces for everything, and trying to recreate nominal typing in a language that does not support it One thing I use sometimes instead of what you're doing there is something like this:
async function getCustomerData(customer: { customerId: number }): Promise<CustomerData> {
// etc...
}
async function getCustomerData(customer: { customerId: number }): Promise<CustomerData> {
// etc...
}
You can look into branded types, but yeah... Personally I prefer to try working with the language, rather than against it
Bence (Arzen)
Bence (Arzen)2mo ago
Thank you for your insight! 🙂 It was very useful, I'll do my best to embrace what the language provides.
Want results from more Discord servers?
Add your server
More Posts