diff --git a/README.md b/README.md
index 3d6baee5..2aacd66c 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,8 @@
- [typanion](#typanion)
- [Ajv](#ajv)
- [TypeBox](#typebox)
+ - [With `ValueCheck`](#with-valuecheck)
+ - [With `TypeCompiler`](#with-typecompiler)
- [ArkType](#arktype)
- [Valibot](#valibot)
- [Backers](#backers)
@@ -486,6 +488,8 @@ JSON Schema Type Builder with Static Type Resolution for TypeScript
[![npm](https://img.shields.io/bundlephobia/minzip/@sinclair/typebox?style=for-the-badge)](https://bundlephobia.com/result?p=@sinclair/typebox)
+#### With `ValueCheck`
+
```typescript jsx
import { useForm } from 'react-hook-form';
import { typeboxResolver } from '@hookform/resolvers/typebox';
@@ -511,6 +515,38 @@ const App = () => {
};
```
+#### With `TypeCompiler`
+
+A high-performance JIT of `TypeBox`, [read more](https://github.com/sinclairzx81/typebox#typecompiler)
+
+```typescript jsx
+import { useForm } from 'react-hook-form';
+import { typeboxResolver } from '@hookform/resolvers/typebox';
+import { Type } from '@sinclair/typebox';
+import { TypeCompiler } from '@sinclair/typebox/compiler';
+
+const schema = Type.Object({
+ username: Type.String({ minLength: 1 }),
+ password: Type.String({ minLength: 1 }),
+});
+
+const typecheck = TypeCompiler.Compile(schema);
+
+const App = () => {
+ const { register, handleSubmit } = useForm({
+ resolver: typeboxResolver(typecheck),
+ });
+
+ return (
+
+ );
+};
+```
+
### [ArkType](https://github.com/arktypeio/arktype)
TypeScript's 1:1 validator, optimized from editor to runtime
diff --git a/typebox/src/__tests__/Form-compiler.tsx b/typebox/src/__tests__/Form-compiler.tsx
new file mode 100644
index 00000000..a94781e1
--- /dev/null
+++ b/typebox/src/__tests__/Form-compiler.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+import { useForm } from 'react-hook-form';
+import { typeboxResolver } from '..';
+import { Type, Static } from '@sinclair/typebox';
+import { TypeCompiler } from '@sinclair/typebox/compiler';
+
+const schema = Type.Object({
+ username: Type.String({ minLength: 1 }),
+ password: Type.String({ minLength: 1 }),
+});
+
+const typecheck = TypeCompiler.Compile(schema)
+
+type FormData = Static & { unusedProperty: string };
+
+interface Props {
+ onSubmit: (data: FormData) => void;
+}
+
+function TestComponent({ onSubmit }: Props) {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: typeboxResolver(typecheck), // Useful to check TypeScript regressions
+ });
+
+ return (
+
+ );
+}
+
+test("form's validation with Typebox (with compiler) and TypeScript's integration", async () => {
+ const handleSubmit = vi.fn();
+ render();
+
+ expect(screen.queryAllByRole('alert')).toHaveLength(0);
+
+ await user.click(screen.getByText(/submit/i));
+
+ expect(
+ screen.getAllByText(/Expected string length greater or equal to 1/i),
+ ).toHaveLength(2);
+
+ expect(handleSubmit).not.toHaveBeenCalled();
+});
diff --git a/typebox/src/__tests__/Form-native-validation-compiler.tsx b/typebox/src/__tests__/Form-native-validation-compiler.tsx
new file mode 100644
index 00000000..c5e74c8c
--- /dev/null
+++ b/typebox/src/__tests__/Form-native-validation-compiler.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+import { typeboxResolver } from '..';
+
+import { Type, Static } from '@sinclair/typebox';
+import { TypeCompiler } from '@sinclair/typebox/compiler';
+
+const schema = Type.Object({
+ username: Type.String({ minLength: 1 }),
+ password: Type.String({ minLength: 1 }),
+});
+
+const typecheck = TypeCompiler.Compile(schema)
+
+type FormData = Static;
+
+interface Props {
+ onSubmit: (data: FormData) => void;
+}
+
+function TestComponent({ onSubmit }: Props) {
+ const { register, handleSubmit } = useForm({
+ resolver: typeboxResolver(typecheck),
+ shouldUseNativeValidation: true,
+ });
+
+ return (
+
+ );
+}
+
+test("form's native validation with Typebox (with compiler)", async () => {
+ const handleSubmit = vi.fn();
+ render();
+
+ // username
+ let usernameField = screen.getByPlaceholderText(
+ /username/i,
+ ) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(true);
+ expect(usernameField.validationMessage).toBe('');
+
+ // password
+ let passwordField = screen.getByPlaceholderText(
+ /password/i,
+ ) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(true);
+ expect(passwordField.validationMessage).toBe('');
+
+ await user.click(screen.getByText(/submit/i));
+
+ // username
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(false);
+ expect(usernameField.validationMessage).toBe(
+ 'Expected string length greater or equal to 1',
+ );
+
+ // password
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(false);
+ expect(passwordField.validationMessage).toBe(
+ 'Expected string length greater or equal to 1',
+ );
+
+ await user.type(screen.getByPlaceholderText(/username/i), 'joe');
+ await user.type(screen.getByPlaceholderText(/password/i), 'password');
+
+ // username
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
+ expect(usernameField.validity.valid).toBe(true);
+ expect(usernameField.validationMessage).toBe('');
+
+ // password
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
+ expect(passwordField.validity.valid).toBe(true);
+ expect(passwordField.validationMessage).toBe('');
+});
diff --git a/typebox/src/__tests__/Form-native-validation.tsx b/typebox/src/__tests__/Form-native-validation.tsx
index 78fdc439..460aa5fe 100644
--- a/typebox/src/__tests__/Form-native-validation.tsx
+++ b/typebox/src/__tests__/Form-native-validation.tsx
@@ -34,7 +34,7 @@ function TestComponent({ onSubmit }: Props) {
);
}
-test("form's native validation with Zod", async () => {
+test("form's native validation with Typebox", async () => {
const handleSubmit = vi.fn();
render();
diff --git a/typebox/src/__tests__/Form.tsx b/typebox/src/__tests__/Form.tsx
index c1f3cc95..ce7a5729 100644
--- a/typebox/src/__tests__/Form.tsx
+++ b/typebox/src/__tests__/Form.tsx
@@ -38,7 +38,7 @@ function TestComponent({ onSubmit }: Props) {
);
}
-test("form's validation with Zod and TypeScript's integration", async () => {
+test("form's validation with Typebox and TypeScript's integration", async () => {
const handleSubmit = vi.fn();
render();
diff --git a/typebox/src/__tests__/__snapshots__/typebox-compiler.ts.snap b/typebox/src/__tests__/__snapshots__/typebox-compiler.ts.snap
new file mode 100644
index 00000000..44e4eec9
--- /dev/null
+++ b/typebox/src/__tests__/__snapshots__/typebox-compiler.ts.snap
@@ -0,0 +1,188 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`typeboxResolver (with compiler) > should return a single error from typeboxResolver when validation fails 1`] = `
+{
+ "errors": {
+ "accessToken": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ "birthYear": {
+ "message": "Expected number",
+ "ref": undefined,
+ "type": "41",
+ },
+ "dateStr": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ "email": {
+ "message": "Expected string to match '^[a-z0-9!#$%&'*+/=?^_\`{|}~-]+(?:\\\\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'",
+ "ref": {
+ "name": "email",
+ },
+ "type": "52",
+ },
+ "enabled": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ "like": [
+ {
+ "id": {
+ "message": "Expected number",
+ "ref": undefined,
+ "type": "41",
+ },
+ "name": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ },
+ ],
+ "password": {
+ "message": "Expected string length greater or equal to 8",
+ "ref": {
+ "name": "password",
+ },
+ "type": "51",
+ },
+ "repeatPassword": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ "tags": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ },
+ "username": {
+ "message": "Required property",
+ "ref": {
+ "name": "username",
+ },
+ "type": "45",
+ },
+ },
+ "values": {},
+}
+`;
+
+exports[`typeboxResolver (with compiler) > should return all the errors from typeboxResolver when validation fails with \`validateAllFieldCriteria\` set to true 1`] = `
+{
+ "errors": {
+ "accessToken": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "45": "Required property",
+ "61": "Expected union value",
+ },
+ },
+ "birthYear": {
+ "message": "Expected number",
+ "ref": undefined,
+ "type": "41",
+ "types": {
+ "41": "Expected number",
+ },
+ },
+ "dateStr": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "20": "Expected Date",
+ "45": "Required property",
+ },
+ },
+ "email": {
+ "message": "Expected string to match '^[a-z0-9!#$%&'*+/=?^_\`{|}~-]+(?:\\\\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'",
+ "ref": {
+ "name": "email",
+ },
+ "type": "52",
+ "types": {
+ "52": "Expected string to match '^[a-z0-9!#$%&'*+/=?^_\`{|}~-]+(?:\\\\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'",
+ },
+ },
+ "enabled": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "14": "Expected boolean",
+ "45": "Required property",
+ },
+ },
+ "like": [
+ {
+ "id": {
+ "message": "Expected number",
+ "ref": undefined,
+ "type": "41",
+ "types": {
+ "41": "Expected number",
+ },
+ },
+ "name": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "45": "Required property",
+ "53": "Expected string",
+ },
+ },
+ },
+ ],
+ "password": {
+ "message": "Expected string length greater or equal to 8",
+ "ref": {
+ "name": "password",
+ },
+ "type": "51",
+ "types": {
+ "51": "Expected string length greater or equal to 8",
+ "52": "Expected string to match '^(.*[A-Za-z\\\\d].*)$'",
+ },
+ },
+ "repeatPassword": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "45": "Required property",
+ "53": "Expected string",
+ },
+ },
+ "tags": {
+ "message": "Required property",
+ "ref": undefined,
+ "type": "45",
+ "types": {
+ "45": "Required property",
+ "6": "Expected array",
+ },
+ },
+ "username": {
+ "message": "Required property",
+ "ref": {
+ "name": "username",
+ },
+ "type": "45",
+ "types": {
+ "45": "Required property",
+ "53": "Expected string",
+ },
+ },
+ },
+ "values": {},
+}
+`;
diff --git a/typebox/src/__tests__/typebox-compiler.ts b/typebox/src/__tests__/typebox-compiler.ts
new file mode 100644
index 00000000..e95d2d85
--- /dev/null
+++ b/typebox/src/__tests__/typebox-compiler.ts
@@ -0,0 +1,38 @@
+import { TypeCompiler } from '@sinclair/typebox/compiler';
+import { typeboxResolver } from '..';
+import { schema, validData, invalidData, fields } from './__fixtures__/data';
+
+const shouldUseNativeValidation = false;
+
+describe('typeboxResolver (with compiler)', () => {
+
+ const typecheck = TypeCompiler.Compile(schema)
+
+ it('should return a single error from typeboxResolver when validation fails', async () => {
+ const result = await typeboxResolver(typecheck)(invalidData, undefined, {
+ fields,
+ shouldUseNativeValidation,
+ });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should return all the errors from typeboxResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
+ const result = await typeboxResolver(typecheck)(invalidData, undefined, {
+ fields,
+ criteriaMode: 'all',
+ shouldUseNativeValidation,
+ });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should validate with success', async () => {
+ const result = await typeboxResolver(typecheck)(validData, undefined, {
+ fields,
+ shouldUseNativeValidation,
+ });
+
+ expect(result).toEqual({ errors: {}, values: validData });
+ });
+});
diff --git a/typebox/src/typebox.ts b/typebox/src/typebox.ts
index 3fe79ae1..cf776525 100644
--- a/typebox/src/typebox.ts
+++ b/typebox/src/typebox.ts
@@ -1,7 +1,8 @@
import { appendErrors, FieldError, FieldErrors } from 'react-hook-form';
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
import type { Resolver } from './types';
-import { Value, ValueError } from '@sinclair/typebox/value';
+import { Value, type ValueError } from '@sinclair/typebox/value';
+import { TypeCheck } from '@sinclair/typebox/compiler';
const parseErrorSchema = (
_errors: ValueError[],
@@ -40,7 +41,7 @@ const parseErrorSchema = (
export const typeboxResolver: Resolver =
(schema) => async (values, _, options) => {
- const errors = Array.from(Value.Errors(schema, values));
+ const errors = Array.from(schema instanceof TypeCheck ? schema.Errors(values) : Value.Errors(schema, values));
options.shouldUseNativeValidation && validateFieldsNatively({}, options);
diff --git a/typebox/src/types.ts b/typebox/src/types.ts
index 917d1868..ecae5cde 100644
--- a/typebox/src/types.ts
+++ b/typebox/src/types.ts
@@ -1,8 +1,9 @@
import { Type } from '@sinclair/typebox';
+import type { TypeCheck } from '@sinclair/typebox/compiler/compiler';
import { FieldValues, ResolverResult, ResolverOptions } from 'react-hook-form';
export type Resolver = >(
- schema: T,
+ schema: T | TypeCheck,
) => (
values: TFieldValues,
context: TContext | undefined,