Zod Essentials - Runtime Type Validation with TypeScript
TypeScript gives you strong type checking at compile time, but it can't guarantee type safety at runtime.
Zod is a library that validates data at runtime, and using it together with TypeScript's compile-time type checking gives you stronger type safety.
Of course, you can also use it in JavaScript.
# Installation and basic usage
Install it like this.
1npm i zod
After installing, prepare a user object for a quick test like this.
12345// User data export const userData = { name: 'Neo', age: 22 }
Next, define a schema to validate the user data.
The z object in Zod gives you many schema methods, such as .object(), .string(), and .number().
Once you have defined the data schema you want, call a method such as .parse() on that schema and pass the user data as an argument.
Zod then checks (validates) at runtime whether the user data matches the schema you defined.
If the data doesn't match the schema, an error is thrown at runtime.
1234567891011121314151617import { z } from 'zod' import { userData } from './user.js' // Define the schema const userSchema = z.object({ name: z.string(), age: z.number() }) // Validate the data and handle errors try { userSchema.parse(userData) } catch (error) { if (error instanceof z.ZodError) { console.error(error.issues) } }
If you use TypeScript, you may need the type of the schema you defined.
You can infer the type with z.infer and get the schema's type as the result.
And with TypeScript's satisfies keyword, you can easily check whether the data satisfies that type.
123456789101112131415161718import { z } from 'zod' import { userData } from './user' // Define the schema and type const userSchema = z.object({ name: z.string(), age: z.number() }) type UserSchema = z.infer<typeof userSchema> // Validate the data and handle errors try { userSchema.parse(userData satisfies UserSchema) // Check that the data satisfies the type } catch (error) { if (error instanceof z.ZodError) { console.error(error.issues) // [...] } }
# Schema validation
With the validation methods available on a schema object, you can check whether the given data matches the schema.
# .parse()
The .parse() method validates data, returns the data if validation succeeds, and throws an error if it fails.
So you may need to handle errors with a try/catch statement.
When an error occurs, you can read the issues array property of the error object to see various error details.
1234567891011121314151617181920212223import { z } from 'zod' import { userData } from './user' // Define const userSchema = z.object({ name: z.string(), age: z.number() }) type User = z.infer<typeof userSchema> try { // Validate const result = userSchema.parse(userData satisfies User) // Success console.log(result) // { name: 'Neo', age: 22 } } catch (error) { // Failure if (error instanceof z.ZodError) { console.error(error.issues) // [...] } }
# .safeParse()
The .safeParse() method validates data and returns a result object that contains success or failure information.
On success, you read the data from the data property of the result (result), and on failure, you handle the error information through the error property.
Because .safeParse() doesn't throw an error even when validation fails, you don't need a try/catch statement.
123456789101112// ... // Validate const result = userSchema.safeParse(userData satisfies User) // Success console.log(result.data) // { name: 'Neo', age: 22 } // Failure if (result.error instanceof z.ZodError) { console.error(error.issues) // [...] }
# .parseAsync()
The .parseAsync() method is similar to the .parse() method we saw earlier, but it validates asynchronously with the await keyword.
Use it when your schema definition needs asynchronous processing.
In the next example, the isNameExists function asynchronously checks whether a user name already exists.
It simply returns whether the name exists after 1 second.
You can also test it by setting the argument of the resolve call to true/false.
123456789101112131415// User data export const userData = { name: 'Neo', age: 22 } // Example async function that checks whether a name exists export function isNameExists(name: string) { return new Promise(resolve => { // fetch(`https://api.heropy.dev/v0/users/exists?name=${name}`) // .then(res => res.json()) // .then(name => setTimeout(() => resolve(Boolean(name)), 1000)) setTimeout(() => resolve(Boolean(name)), 1000) }) }
In the next example, we validate that the user name (name) is a string, and also use the isNameExists function to check whether the name already exists.
Because the isNameExists function returns a Promise instance, you need the parseAsync method to wait for the validation.
123456789101112131415161718192021222324252627import { z } from 'zod' import { userData, isNameExists } from './user' const userSchema = z.object({ name: z .string() .refine( val => isNameExists(val), { error: 'This name is already taken.' } ), age: z.number() }) type User = z.infer<typeof userSchema> try { // Validate const res = await userSchema.parseAsync(userData satisfies User) // Success console.log(res) // { name: 'Neo', age: 22 } } catch (error) { // Failure if (error instanceof z.ZodError) { console.error(error.issues[0]) // { code: 'custom', path: ['name'], message: 'This name is already taken.' } } }
# .safeParseAsync()
Like the .safeParse() method we saw earlier, the .safeParseAsync() method returns a result object without a try/catch statement, and like .parseAsync(), you call it asynchronously with the await keyword.
123456789101112// ... // Validate const result = await userSchema.safeParseAsync(userData satisfies User) // Success console.log(result.data) // { name: 'Neo', age: 22 } // Failure if (result.error instanceof z.ZodError) { console.error(result.error.issues[0]) // { code: 'custom', path: ['name'], message: 'This name is already taken.' } }
# Defining schemas
To validate data, you first need to define a schema.
A schema can represent many types, from simple primitives to complex nested objects.
# String types
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980// String z.string() z.string().min(2) // Minimum length z.string().max(10) // Maximum length z.string().length(7) // Exact length z.string().regex(/^[a-z]+$/) // Regular expression z.string().startsWith('A') // Starts with the string z.string().endsWith('Z') // Ends with the string z.string().includes('abc') // Contains the string z.string().uppercase() // Uppercase z.string().lowercase() // Lowercase // Template literal z.templateLiteral(['Hello ', z.string().min(8), '?!']) // => `Hello ${'template'}?!` // => `Hello ${'world'}?!` // ❌ // Email z.email() // => 'thesecon@gmail.com' z.email({ pattern: /^\w+@\w+\.\w+$/ }) // Regular expression z.email({ pattern: z.regexes.email }) // Default email z.email({ pattern: z.regexes.html5Email }) // HTML5 email z.email({ pattern: z.regexes.rfc5322Email }) // RFC 5322 email z.email({ pattern: z.regexes.unicodeEmail }) // Unicode email // URL z.url() // => 'https://heropy.dev', 'http://localhost:3000', 'mailto:thesecon@gmail.com' // Hostname z.hostname() // => 'example.com', 'localhost', '127.0.0.1' // Emoji z.emoji() // => '👍' // Base64 z.base64() // Standard version z.base64url() // URL-safe version // => 'SGVsbG8gd29ybGQ=' // => 'data:text/plain;base64,SGVsbG8gd29ybGQ=' // ❌ // JSON Web Token z.jwt() // => 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30.' // UUID z.uuid() z.uuid({ version: 'v4' }) // Specify the version, v1-8 z.uuidv4() // v4 z.uuidv6() // v6 z.uuidv7() // v7 // => '123e4567-e89b-12d3-a456-426614174000' // Random string (21+ characters) z.nanoid() // => 'abcdefghij12345678901' // Collision-resistant ID (9+ characters) z.cuid() // => 'c12345678' // 128-bit ID (26 characters) z.ulid() // => '01H7Z8K4P2M9N3Q59V0W1X2Y3Z' // IPv4, IPv6 z.ipv4() // => '192.168.1.1' z.ipv6() // => '2001:0db8:85a3:0000:0000:8a2e:0370:7334' z.cidrv4() // => '192.168.1.0/24' z.cidrv6() // => '2001:0db8:85a3:0000:0000:8a2e:0370:7334/64' // Date z.date() // => new Date('2025-12-16') z.date().min(new Date('2025-01-01')).max(new Date('2025-12-31')) // Limit the date range // => new Date('2025-12-16') // => new Date('2026-04-03') // ❌ z.iso.date() // ISO 8601 date z.iso.time() // ISO 8601 time z.iso.datetime() // ISO 8601 date and time z.iso.duration() // ISO 8601 duration (period) // =>'2025-04-03' // =>'12:00', '12:00:00', '12:00:00.000' // =>'2025-04-03T12:00:00Z' // =>'P1Y2M3DT4H5M6S'
# Number types
12345678910111213141516171819202122232425262728293031323334// Number z.number() z.number().gt(5) // Greater than z.number().gte(5) // Greater than or equal to z.number().min(5) // Minimum, same as `gte` z.number().lt(5) // Less than z.number().lte(5) // Less than or equal to z.number().max(5) // Maximum, same as `lte` z.number().positive() // Positive z.number().nonpositive() // Not positive z.number().negative() // Negative z.number().nonnegative() // Not negative z.number().multipleOf(2) // Multiple of z.number().step(2) // Multiple of, same as `multipleOf` // Safe integer z.int() // => ~9007199254740991 // 32-bit integer z.int32() // => ~2147483647 // Big integer z.bigint() // => 1234n z.int64() // 64-bit integer, same as `bigint` // Floating point z.float32() // 32-bit single precision, about 7 digits z.float64() // 64-bit double precision, about 15 digits // Not a Number z.nan() // => NaN
# Other primitive types
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// Boolean z.boolean() // String boolean const stb1 = z.stringbool() stb1.parse('true') // true stb1.parse('1') // true stb1.parse('yes') // true stb1.parse('false') // false stb1.parse('0') // false // The defaults of `stringbool` are as follows z.stringbool({ truthy: ['true', '1', 'yes', 'on', 'y', 'enabled'], // Strings treated as true falsy: ['false', '0', 'no', 'off', 'n', 'disabled'], // Strings treated as false case: 'insensitive' // Case-insensitive }) const stb2 = z.stringbool({ truthy: ['Yes', 'Y'], falsy: ['No', 'N'], case: 'sensitive' // Case-sensitive }) // Custom stb2.parse('Yes') // true stb2.parse('yes') // true stb2.parse('N') // false // Symbol z.symbol() // => Symbol('id') // Null, Undefined z.null() // => null z.undefined() // => undefined z.void() // => void, same as undefined // Can be undefined - z.optional(schema) z.optional(z.number()) // => 123, undefined // => null // ❌ // Can be null - z.nullable(schema) z.nullable(z.number()) // => 123, null // => undefined // ❌ // Can be null or undefined - z.nullish(schema) z.nullish(z.number()) // => 123, null, undefined
# Reference types
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980// Object - strips unknown properties - z.object(object_schema) const obj1 = z.object({ name: z.string(), age: z.number() }) obj1.parse({ name: 'Neo' }) // ❌ obj1.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj1.parse({ name: 'Neo', age: 22, isValid: true }) // { name: 'Neo', age: 22 }, the isValid property is removed // Object - errors on unknown properties - z.strictObject(object_schema) const obj2 = z.strictObject({ name: z.string(), age: z.number() }) obj2.parse({ name: 'Neo' }) // ❌ obj2.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj2.parse({ name: 'Neo', age: 22, isValid: true }) // ❌ // Object - allows unknown properties - z.looseObject(object_schema) const obj3 = z.looseObject({ name: z.string(), age: z.number() }) obj3.parse({ name: 'Neo' }) // ❌ obj3.parse({ name: 'Neo', age: 22 }) // { name: 'Neo', age: 22 } obj3.parse({ name: 'Neo', age: 22, isValid: true }) // { name: 'Neo', age: 22, isValid: true }, the isValid property is kept // Object methods const obj4 = z.object({ name: z.string(), age: z.number().optional() // Defined as an optional property }) obj4.catchAll(z.boolean()) // Schema for the unknown extra properties you want to allow // => { isValid: z.boolean(), hasEmail: z.boolean() } obj4.shape.age // Schema of a property // z.number().optional() obj4.keyof() // Schema (enum) of all property names of the object // z.enum(['name', 'age']) obj4.extend({ isValid: z.boolean() }) // New merged object schema // z.object({ name: z.string(), age: z.number().optional(), isValid: z.boolean() }) obj4.pick({ name: true }) // Object schema with only the picked properties // z.object({ name: z.string() }) obj4.omit({ name: true }) // Object schema without the omitted properties // z.object({ age: z.number().optional() }) obj4.partial() // New object schema where every property is optional // z.object({ name: z.string().optional(), age: z.number().optional() }) obj4.required() // New object schema where every property is required // z.object({ name: z.string(), age: z.number() }) // Object - key-value - z.record(key_schema, value_schema) z.record(z.string(), z.number()) // => { a: 1, b: 2, c: 3 } // => { a: '1', b: '2', c: '3' } // ❌ // => {} // ❌ // Restrict the key types z.record( z.union([z.string(), z.number(), z.symbol()]), z.number() ) // => { abc: 1, 123: 2, [Symbol('id')]: 3 } // Restrict the key names z.record( z.enum(['name', 'age']), z.number() ) // => { name: 1, age: 2 } // => { name: 1, age: 2, isValid: 3 } // ❌ // Object - key-value with optional properties - z.partialRecord(key_schema, value_schema) z.partialRecord(z.string(), z.number()) // => {} // => { a: 1 } // => { a: 1, b: 2 } // => { a: 1, b: 2, c: 3 } // Array - z.array(item_schema) z.array(z.string()) // => ['A', 'B', 'C', 'D'] z.array(z.number()) // => [1, 2, 3, 4] z.array(z.union([z.string(), z.number()])) // => ['A', 1, 'B', 2, 'C', 3, 'D', 4] z.array(z.string()).min(3) // Minimum number of items z.array(z.string()).max(2) // Maximum number of items z.array(z.number()).unwrap() // Get the item schema of the array // Tuple - z.tuple(schema_array) z.tuple([z.string(), z.number()]) // => ['Neo', 22]
# Other types
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061// Literal - z.literal(value) z.literal('Hello') z.literal(123) z.literal(true) // Any type z.any() // Unknown type z.unknown() // Impossible type z.never() // Enum - z.enum(string_array) z.enum(['a', 'b']) // => 'a' // => 'b' // => 'c' // ❌ // Union - z.union(schema_array) z.union([z.string(), z.number()]) // => 'Hello' // => 123 // => true // ❌ // Discriminated union - z.discriminatedUnion(discriminator_key, schema_array) const dis1 = z.discriminatedUnion('status', [ z.object({ status: z.literal('success'), data: z.string() }), z.object({ status: z.literal('failed'), error: z.string() }) ]) type DU = z.infer<typeof dis1> // The DU type is as follows // { status: 'success'; data: string } | { status: 'failed'; error: string } function getData(res: DU) { dis1.parse(res) return res.status === 'success' ? res.data : res.error } getData({ status: 'success', data: 'Hello~' }) // 'Hello~' getData({ status: 'failed', error: 'Error~' }) // 'Error~' // Intersection - z.intersection(schema_array) const obj1 = z.object({ status: z.string(), data: z.string() }) const obj2 = z.object({ status: z.string(), error: z.string() }) const int1 = z.intersection(obj1, obj2) int1.parse({ status: 'success', data: 'Hello!', error: 'Error!' }) // JSON z.json() // => 'Hello' // => 123 // => { name: 'Neo', age: 22 } // => undefined // ❌ // => function () {} // ❌ // => new Date() // ❌
# Class types
123456789101112131415161718192021222324252627282930313233343536373839class User { constructor(public name: string, public age: number) {} } // Instance - z.instanceof(class) const user = new User('Neo', 22) const date = new Date() const ins1 = z.instanceof(User) ins1.parse(user) ins1.parse(date) // ❌ // Property - z.property(property_name, schema) const pro1 = z.instanceof(User).check(z.property('age', z.number().min(18))) pro1.parse(new User('Neo', 22)) pro1.parse(new User('Neo', 17)) // ❌ // Map - z.map(key_schema, value_schema) const map = new Map() map.set(200, 'OK') map.set(404, 'Not Found') map.set(500, 'Internal Server Error') const ma1 = z.map(z.number(), z.string()) ma1.parse(map) // Set - z.set(value_schema) const set = new Set() set.add({ x: 123 }) const se1 = z.set(z.object({ x: z.number() })) se1.parse(set) // File z.file() z.file().min(1024) // Minimum size (bytes) z.file().max(1024) // Maximum size (bytes) z.file().mime('image/png') // MIME type const file = new File([], 'favicon.png') const fil1 = z.file() fil1.parse(file)
# Custom types
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130// Preprocess before the schema - z.preprocess(preprocess_function, schema) const pre1 = z.preprocess(val => { return typeof val === 'string' ? val.toUpperCase() : val }, z.string()) pre1.parse('abc') // 'ABC' pre1.parse(123) // ❌ // Custom schema - z.custom(validation_function) const cus1 = z.custom(val => { return typeof val === 'string' ? /[A-Z]/.test(val) : false }) cus1.parse('ABC') // 'ABC' cus1.parse(123) // ❌ // Transform - z.transform(transform_function) const tra1 = z.transform(val => { return typeof val === 'string' ? val.toUpperCase() : val }) tra1.parse('abc') // 'ABC' tra1.parse(123) // 123 // schema.transform(transform_function) const tra2 = z.string().transform(val => val.toUpperCase()) tra2.parse('abc') // 'ABC' tra2.parse(123) // ❌ // Connect schemas - z.pipe(in_schema, out_schema) const pip1 = z.pipe( z.string(), z.transform(val => val.length) ) pip1.parse('abc') // 3 pip1.parse(123) // ❌ // in_schema.pipe(out_schema) const pip2 = z.string().pipe(z.transform(val => val.length)) pip2.parse('abc') // 3 pip2.parse(123) // ❌ // Refine a schema - schema.refine(refine_function, options) const ref1 = z .string() .refine(val => val.length < 4, { error: 'Too many letters!' }) .refine(val => val.toUpperCase().includes('A'), { error: `Must include the letter 'A' or 'a'!` }) ref1.parse('abc') // 'abc' ref1.parse('xyz123') // ❌ // Refine a schema - schema.superRefine(refine_function) const sup1 = z.string().superRefine((val, ctx) => { if (val.length > 3) { ctx.addIssue({ code: 'custom', message: 'Too many letters!' }) } if (!val.toUpperCase().includes('A')) { ctx.addIssue({ code: 'custom', message: `must include the letter 'A' or 'a'.` }) } }) sup1.parse('abc') // 'abc' sup1.parse('xyz123') // ❌ // Check a schema - schema.check(check_function) const che1 = z.string().check(ctx => { if (ctx.value.length > 3) { ctx.issues.push({ code: 'custom', input: ctx.value, message: 'Too many letters!' }) } if (!ctx.value.toUpperCase().includes('A')) { ctx.issues.push({ code: 'custom', input: ctx.value, message: `must include the letter 'A' or 'a'.` }) } }) che1.parse('abc') // 'abc' che1.parse('xyz123') // ❌ // Default value - schema.default(default_value) const def1 = z .number() .transform(val => val + 10) .default(0) def1.parse(2) // 12 def1.parse(undefined) // 0 // Default value that goes through parsing - schema.prefault(default_value) const pre1 = z .number() .transform(val => val + 10) .prefault(0) pre1.parse(2) // 12 pre1.parse(undefined) // 10 // Fallback value on error const cat1 = z.number().catch(0) cat1.parse('Hello~') // 0 const cat2 = z.number().catch(ctx => { console.log(ctx.error.issues[0].message) // 'Invalid input: expected number, received string' return 0 }) cat2.parse('Hello~') // 0 // Readonly const arr = z.array(z.number()).readonly() const res = schema.parse([1, 2, 3]) res // [1, 2, 3] res.push(4) // ❌ // Error! ts(2339) // Branded type const id = z.email().brand<'ID'>() const pw = z.string().min(8).max(20).brand<'PW'>() type Id = z.infer<typeof id> type Pw = z.infer<typeof pw> const res1: Pw = id.parse('thesecon@gmail.com') // Error! ts(2322) const res2: Id = pw.parse('qwer1234') // Error! ts(2322) res1 // 'thesecon@gmail.com' res2 // 'qwer1234'
# Error handling
As we saw above, when you use a validation method such as .parse() or .safeParse() and the data fails validation, you can inspect the error.
The error object of a failed result is an instance of z.ZodError, and its issues property holds several issue objects as array items.
An issue object has properties such as expected and code, and in particular, its message property tells you why the error happened.
12345678910111213141516171819202122// .parse() + try/catch try { z.number().parse('Hello~') // ❌ } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues) } } // .safeParse() const result = z.number().safeParse('Hello~') // ❌ if (result.error instanceof z.ZodError) { console.log(result.error.issues) } // Example issue object structure, error.issues[0] // => { // expected: 'number', // Expected type // code: 'invalid_type', // Error code // path: ['age'], // Path (property) where the error occurred // message: 'Invalid input: expected number, received string' // Error message // }
Zod supports internationalization.
So you can also print the default error messages in Korean.
You can also add your own error messages directly to a schema or to a validation method.
Note that a custom error message on the schema takes precedence over one on the validation method.
1234567891011121314151617181920212223import { z } from 'zod' import { ko } from 'zod/locales' z.config(ko()) // Korean locale try { z.number().parse('Hello~') // '잘못된 입력: 예상 타입은 number, 받은 타입은 string입니다' (Korean for 'Invalid input: expected number, received string') z.number('High priority').parse('Hello~') z.number({ error: 'High priority' }).parse('Hello~') z.number({ error: issue => 'High priority' }).parse('Hello~') z.number({ error: issue => 'High priority' }).parse('Hello~', { error: issue => 'Low priority' }) // 'High priority' z.number().parse('Hello~', { error: issue => 'Low priority' }) // 'Low priority' } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues[0].message) } }
To display error information more flexibly, Zod provides utility methods such as .treeifyError().
| Member | Description |
|---|---|
| error.issues | Shows the errors as an array |
| z.treeifyError(error) | Shows the errors as a tree |
| z.prettifyError(error) | Shows the errors as human-readable text |
| z.flattenError(error) | Shows the errors in a clean, flat shape |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172import { z } from 'zod' import { en } from 'zod/locales' z.config(en()) try { z.strictObject({ name: z.string(), age: z.number() }).parse({ name: 123, age: 'Neo', isValid: true }) } catch (error) { if (error instanceof z.ZodError) { console.log(error.issues) // Shows the errors as an array // => [ // { // expected: 'string', // code: 'invalid_type', // path: [ 'name' ], // message: 'Invalid input: expected string, received number' // }, // { // expected: 'number', // code: 'invalid_type', // path: [ 'age' ], // message: 'Invalid input: expected number, received string' // }, // { // code: 'unrecognized_keys', // keys: [ 'isValid' ], // path: [], // message: 'Unrecognized key: "isValid"' // } // ] console.log(z.treeifyError(error)) // Shows the errors as a tree // => { // errors: [ 'Unrecognized key: "isValid"' ], // properties: { // name: { // errors: [ 'Invalid input: expected string, received number' ] // }, // age: { // errors: [ 'Invalid input: expected number, received string' ] // } // } // } console.log(z.prettifyError(error)) // Shows the errors as human-readable text // => ✖ Unrecognized key: 'isValid' // ✖ Invalid input: expected string, received number // → at name // ✖ Invalid input: expected number, received string // → at age console.log(z.flattenError(error)) // Shows the errors in a clean, flat shape // => { // formErrors: [ 'Unrecognized key: "isValid"' ], // fieldErrors: { // name: [ 'Invalid input: expected string, received number' ], // age: [ 'Invalid input: expected number, received string' ] // } // } } }
# Examples
# Validating a login form
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586import { useState } from 'react' import { z } from 'zod' const schema = z.object({ id: z.email({ error: 'The email format is not valid.' }), pw: z.string().min(5, { error: issue => `The password must be at least ${issue.minimum} characters.` }) }) function TextField( props: React.InputHTMLAttributes<HTMLInputElement> & { error: string } ) { return ( <div className="mb-3"> <input className="w-full rounded-md border-2 border-gray-300 p-2" {...props} /> {props.error && ( <div className="mt-1 text-sm text-red-500">{props.error}</div> )} </div> ) } export default function App() { const [id, setId] = useState('') const [pw, setPw] = useState('') const [idError, setIdError] = useState('') const [pwError, setPwError] = useState('') function validateFields() { setIdError('') setPwError('') const result = schema.safeParse({ id, pw }) if (result.error instanceof z.ZodError) { result.error.issues.forEach(issue => { switch (issue.path[0]) { case 'id': setIdError(issue.message) break case 'pw': setPwError(issue.message) break } }) return false } return true } function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!validateFields()) return // Handle the login.. console.log('Logged in!') } return ( <> <form className="max-w-md px-4 py-6" onSubmit={handleSubmit}> <TextField placeholder="Enter your email." type="text" value={id} onChange={e => setId(e.target.value)} error={idError} /> <TextField placeholder="Enter your password." type="password" value={pw} onChange={e => setPw(e.target.value)} error={pwError} /> <button className="w-full cursor-pointer rounded-md bg-blue-500 p-2 text-white hover:bg-blue-600" type="submit"> Log in </button> </form> </> ) }
Thanks for reading to the end.
Likes and kind comments help keep this blog going!