or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced.mdfield-api.mdform-api.mdframework-integrations.mdhooks.mdindex.mdvalidation.md

validation.mddocs/

0

# Validation System

1

2

Comprehensive validation system supporting synchronous and asynchronous validators, Standard Schema integration for third-party validation libraries, custom validation logic, and detailed error handling with multiple validation triggers.

3

4

## Capabilities

5

6

### Standard Schema Integration

7

8

TanStack React Form implements the Standard Schema specification for validation library integration.

9

10

```typescript { .api }

11

/**

12

* Standard Schema validator interface (v1)

13

* Implemented by validation libraries like Zod, Yup, Valibot, Joi, etc.

14

*/

15

interface StandardSchemaV1<Input = unknown, Output = Input> {

16

readonly '~standard': StandardSchemaV1.Props<Input, Output>;

17

}

18

19

namespace StandardSchemaV1 {

20

interface Props<Input, Output> {

21

/** Standard Schema version */

22

readonly version: 1;

23

24

/** Library vendor identifier */

25

readonly vendor: string;

26

27

/**

28

* Validation function

29

* @param value - Value to validate

30

* @returns Validation result with output value or issues

31

*/

32

readonly validate: (

33

value: unknown,

34

) => Result<Output> | Promise<Result<Output>>;

35

}

36

37

interface Result<Output> {

38

/** Validated and transformed output value (present if valid) */

39

readonly value?: Output;

40

41

/** Validation issues/errors (present if invalid) */

42

readonly issues?: ReadonlyArray<StandardSchemaV1Issue>;

43

}

44

}

45

46

/**

47

* Standard Schema issue/error interface

48

*/

49

interface StandardSchemaV1Issue {

50

/** Human-readable error message */

51

readonly message: string;

52

53

/** Path to the field with error (e.g., 'user.email') */

54

readonly path?: ReadonlyArray<string | number | symbol>;

55

}

56

57

/**

58

* Type guard to check if a validator is a Standard Schema validator

59

* @param validator - Validator to check

60

* @returns True if validator implements Standard Schema

61

*/

62

function isStandardSchemaValidator(

63

validator: unknown,

64

): validator is StandardSchemaV1;

65

```

66

67

### Standard Schema Validators

68

69

Helper object for working with Standard Schema validators.

70

71

```typescript { .api }

72

/**

73

* Standard Schema validation helpers

74

*/

75

const standardSchemaValidators: {

76

/**

77

* Synchronously validates a value using a Standard Schema validator

78

* @param value - Value to validate

79

* @param schema - Standard Schema validator

80

* @returns Validation error or undefined if valid

81

*/

82

validate<TInput, TOutput>(

83

value: TInput,

84

schema: StandardSchemaV1<TInput, TOutput>,

85

): ValidationError | undefined;

86

87

/**

88

* Asynchronously validates a value using a Standard Schema validator

89

* @param value - Value to validate

90

* @param schema - Standard Schema validator

91

* @returns Promise resolving to validation error or undefined if valid

92

*/

93

validateAsync<TInput, TOutput>(

94

value: TInput,

95

schema: StandardSchemaV1<TInput, TOutput>,

96

): Promise<ValidationError | undefined>;

97

};

98

```

99

100

### Validation Logic Functions

101

102

Control when and how validation runs with custom logic functions.

103

104

```typescript { .api }

105

/**

106

* Default validation logic that runs validators based on event type

107

* Runs onMount, onChange, onBlur, onSubmit validators at their respective triggers

108

*/

109

const defaultValidationLogic: ValidationLogicFn;

110

111

/**

112

* Validation logic similar to React Hook Form

113

* Only validates dynamically after first submission attempt

114

*

115

* @param props.mode - Validation mode before submission ('change' | 'blur' | 'submit')

116

* @param props.modeAfterSubmission - Validation mode after submission ('change' | 'blur' | 'submit')

117

* @returns Validation logic function

118

*/

119

function revalidateLogic(props?: {

120

mode?: 'change' | 'blur' | 'submit';

121

modeAfterSubmission?: 'change' | 'blur' | 'submit';

122

}): ValidationLogicFn;

123

124

/**

125

* Validation logic function type

126

* Determines validation behavior based on form state and trigger cause

127

* @param props.cause - Reason for validation

128

* @param props.validator - Validator type to check

129

* @param props.value - Current value

130

* @param props.formApi - Form API instance

131

*/

132

type ValidationLogicFn = (props: ValidationLogicProps) => void;

133

134

interface ValidationLogicProps {

135

/** Reason for validation trigger */

136

cause: ValidationCause;

137

138

/** Type of validator being checked */

139

validator: ValidationErrorMapKeys;

140

141

/** Current form or field value */

142

value: unknown;

143

144

/** Form API instance */

145

formApi: AnyFormApi;

146

}

147

```

148

149

### Validation Types

150

151

Core types for validation errors and triggers.

152

153

```typescript { .api }

154

/** Validation error - can be any type (string, object, etc.) */

155

type ValidationError = unknown;

156

157

/** Validation trigger cause */

158

type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server' | 'dynamic';

159

160

/** Listener trigger cause (subset of ValidationCause) */

161

type ListenerCause = 'change' | 'blur' | 'submit' | 'mount';

162

163

/** Validation source (form-level or field-level) */

164

type ValidationSource = 'form' | 'field';

165

166

/** Keys for validation error maps (e.g., 'onMount', 'onChange', 'onBlur') */

167

type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`;

168

169

/**

170

* Map of validation errors by trigger type

171

*/

172

type ValidationErrorMap = Partial<Record<ValidationErrorMapKeys, ValidationError>>;

173

174

/**

175

* Map of validation error sources by trigger type

176

*/

177

type ValidationErrorMapSource = Partial<Record<ValidationErrorMapKeys, ValidationSource>>;

178

179

/**

180

* Form-level validation error map

181

* Extends ValidationErrorMap to support global form errors

182

*/

183

type FormValidationErrorMap<

184

TFormData,

185

TOnMount extends undefined | FormValidateOrFn<TFormData>,

186

TOnChange extends undefined | FormValidateOrFn<TFormData>,

187

TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,

188

TOnBlur extends undefined | FormValidateOrFn<TFormData>,

189

TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,

190

TOnSubmit extends undefined | FormValidateOrFn<TFormData>,

191

TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,

192

TOnDynamic extends undefined | FormValidateOrFn<TFormData>,

193

TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,

194

TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

195

> = {

196

onMount?: UnwrapFormValidateOrFn<TOnMount>;

197

onChange?: UnwrapFormValidateOrFn<TOnChange>;

198

onChangeAsync?: UnwrapFormAsyncValidateOrFn<TOnChangeAsync>;

199

onBlur?: UnwrapFormValidateOrFn<TOnBlur>;

200

onBlurAsync?: UnwrapFormAsyncValidateOrFn<TOnBlurAsync>;

201

onSubmit?: UnwrapFormValidateOrFn<TOnSubmit>;

202

onSubmitAsync?: UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>;

203

onDynamic?: UnwrapFormValidateOrFn<TOnDynamic>;

204

onDynamicAsync?: UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>;

205

onServer?: UnwrapFormAsyncValidateOrFn<TOnServer>;

206

};

207

```

208

209

### Global Form Validation Errors

210

211

Support for validation errors that affect both form-level and field-level state.

212

213

```typescript { .api }

214

/**

215

* Global form validation error

216

* Can specify both form-level error and field-specific errors

217

*/

218

interface GlobalFormValidationError<TFormData> {

219

/** Form-level error message */

220

form?: ValidationError;

221

222

/** Map of field-specific errors by field path */

223

fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>;

224

}

225

226

/**

227

* Form validation error type

228

* Can be either a simple error or a global error with field mapping

229

*/

230

type FormValidationError<TFormData> =

231

| ValidationError

232

| GlobalFormValidationError<TFormData>;

233

234

/**

235

* Type guard to check if error is a global form validation error

236

* @param error - Error to check

237

* @returns True if error is GlobalFormValidationError

238

*/

239

function isGlobalFormValidationError(

240

error: unknown,

241

): error is GlobalFormValidationError<any>;

242

243

/** Extracts the form-level error from a GlobalFormValidationError */

244

type ExtractGlobalFormError<T> = T extends GlobalFormValidationError<any>

245

? T['form']

246

: T;

247

```

248

249

### Validator Helper Types

250

251

Types for unwrapping validator return values.

252

253

```typescript { .api }

254

/** Unwraps return type from form validator */

255

type UnwrapFormValidateOrFn<T> = T extends FormValidateFn<any>

256

? ReturnType<T> extends Promise<infer U>

257

? U

258

: ReturnType<T>

259

: T extends StandardSchemaV1<any, any>

260

? ValidationError

261

: undefined;

262

263

/** Unwraps return type from async form validator */

264

type UnwrapFormAsyncValidateOrFn<T> = T extends FormValidateAsyncFn<any>

265

? Awaited<ReturnType<T>>

266

: T extends StandardSchemaV1<any, any>

267

? ValidationError

268

: undefined;

269

270

/** Unwraps return type from field validator */

271

type UnwrapFieldValidateOrFn<T> = T extends FieldValidateFn<any, any, any>

272

? ReturnType<T> extends Promise<infer U>

273

? U

274

: ReturnType<T>

275

: T extends StandardSchemaV1<any, any>

276

? ValidationError

277

: undefined;

278

279

/** Unwraps return type from async field validator */

280

type UnwrapFieldAsyncValidateOrFn<T> = T extends FieldValidateAsyncFn<any, any, any>

281

? Awaited<ReturnType<T>>

282

: T extends StandardSchemaV1<any, any>

283

? ValidationError

284

: undefined;

285

```

286

287

### Validator Interface Types

288

289

```typescript { .api }

290

/** Asynchronous validator interface */

291

interface AsyncValidator<TInput> {

292

(value: TInput, signal: AbortSignal): Promise<ValidationError | undefined>;

293

}

294

295

/** Synchronous validator interface */

296

interface SyncValidator<TInput> {

297

(value: TInput): ValidationError | undefined;

298

}

299

```

300

301

## Usage Examples

302

303

### Using Zod Schema Validation

304

305

```typescript

306

import { useForm } from '@tanstack/react-form';

307

import { z } from 'zod';

308

309

const userSchema = z.object({

310

name: z.string().min(2, 'Name must be at least 2 characters'),

311

email: z.string().email('Invalid email address'),

312

age: z.number().min(18, 'Must be at least 18 years old'),

313

});

314

315

function UserForm() {

316

const form = useForm({

317

defaultValues: {

318

name: '',

319

email: '',

320

age: 0,

321

},

322

validators: {

323

onChange: userSchema,

324

},

325

});

326

327

return (

328

<form onSubmit={(e) => {

329

e.preventDefault();

330

form.handleSubmit();

331

}}>

332

<form.Field name="email" validators={{ onChange: z.string().email() }}>

333

{(field) => (

334

<div>

335

<input

336

value={field.state.value}

337

onChange={(e) => field.handleChange(e.target.value)}

338

/>

339

{field.state.meta.errors[0] && (

340

<span>{field.state.meta.errors[0]}</span>

341

)}

342

</div>

343

)}

344

</form.Field>

345

</form>

346

);

347

}

348

```

349

350

### Custom Validation Functions

351

352

```typescript

353

import { useForm } from '@tanstack/react-form';

354

355

function PasswordForm() {

356

const form = useForm({

357

defaultValues: {

358

password: '',

359

confirmPassword: '',

360

},

361

validators: {

362

onChange: ({ value }) => {

363

if (value.password !== value.confirmPassword) {

364

return {

365

form: 'Passwords do not match',

366

fields: {

367

confirmPassword: 'Must match password',

368

},

369

};

370

}

371

return undefined;

372

},

373

},

374

});

375

376

return (

377

<form.Field

378

name="password"

379

validators={{

380

onChange: ({ value }) => {

381

if (value.length < 8) {

382

return 'Password must be at least 8 characters';

383

}

384

if (!/[A-Z]/.test(value)) {

385

return 'Password must contain an uppercase letter';

386

}

387

if (!/[0-9]/.test(value)) {

388

return 'Password must contain a number';

389

}

390

return undefined;

391

},

392

}}

393

>

394

{(field) => (

395

<div>

396

<input

397

type="password"

398

value={field.state.value}

399

onChange={(e) => field.handleChange(e.target.value)}

400

/>

401

{field.state.meta.errors[0]}

402

</div>

403

)}

404

</form.Field>

405

);

406

}

407

```

408

409

### Async Validation with Debouncing

410

411

```typescript

412

function UsernameField() {

413

const form = useForm({

414

defaultValues: {

415

username: '',

416

},

417

asyncDebounceMs: 500,

418

});

419

420

return (

421

<form.Field

422

name="username"

423

validators={{

424

onChange: ({ value }) => {

425

if (value.length < 3) {

426

return 'Username must be at least 3 characters';

427

}

428

return undefined;

429

},

430

onChangeAsync: async ({ value, signal }) => {

431

try {

432

const response = await fetch(

433

`/api/check-username?username=${value}`,

434

{ signal }

435

);

436

const data = await response.json();

437

return data.available ? undefined : 'Username already taken';

438

} catch (error) {

439

if (error.name === 'AbortError') {

440

return undefined; // Validation was cancelled

441

}

442

throw error;

443

}

444

},

445

}}

446

>

447

{(field) => (

448

<div>

449

<input

450

value={field.state.value}

451

onChange={(e) => field.handleChange(e.target.value)}

452

/>

453

{field.state.meta.isValidating && <span>Checking...</span>}

454

{field.state.meta.errors[0] && (

455

<span>{field.state.meta.errors[0]}</span>

456

)}

457

</div>

458

)}

459

</form.Field>

460

);

461

}

462

```

463

464

### Custom Validation Logic

465

466

```typescript

467

import { useForm, revalidateLogic } from '@tanstack/react-form';

468

469

function ReactHookFormStyleValidation() {

470

const form = useForm({

471

defaultValues: {

472

email: '',

473

},

474

// Only validate on blur before submission, on change after submission

475

validationLogic: revalidateLogic({

476

mode: 'blur',

477

modeAfterSubmission: 'change',

478

}),

479

validators: {

480

onBlur: ({ value }) => {

481

if (!value.email.includes('@')) {

482

return { form: 'Invalid email' };

483

}

484

return undefined;

485

},

486

onChange: ({ value }) => {

487

if (!value.email.includes('@')) {

488

return { form: 'Invalid email' };

489

}

490

return undefined;

491

},

492

},

493

});

494

495

return <form>{/* fields */}</form>;

496

}

497

```

498

499

### Multiple Validators Per Event

500

501

```typescript

502

function MultiValidatorField() {

503

const form = useForm({

504

defaultValues: {

505

email: '',

506

},

507

});

508

509

return (

510

<form.Field

511

name="email"

512

validators={{

513

onChange: [

514

// Array of validators - all must pass

515

({ value }) => (!value ? 'Email is required' : undefined),

516

({ value }) => (!value.includes('@') ? 'Must include @' : undefined),

517

({ value }) => (!value.includes('.') ? 'Must include domain' : undefined),

518

],

519

}}

520

>

521

{(field) => (

522

<div>

523

<input

524

value={field.state.value}

525

onChange={(e) => field.handleChange(e.target.value)}

526

/>

527

{field.state.meta.errors.map((error, i) => (

528

<div key={i}>{String(error)}</div>

529

))}

530

</div>

531

)}

532

</form.Field>

533

);

534

}

535

```

536

537

### Conditional Field Validation

538

539

```typescript

540

function ConditionalValidation() {

541

const form = useForm({

542

defaultValues: {

543

shippingMethod: 'standard',

544

trackingNumber: '',

545

},

546

});

547

548

return (

549

<>

550

<form.Field name="shippingMethod">

551

{(field) => (

552

<select

553

value={field.state.value}

554

onChange={(e) => field.handleChange(e.target.value)}

555

>

556

<option value="standard">Standard</option>

557

<option value="express">Express</option>

558

</select>

559

)}

560

</form.Field>

561

562

<form.Field

563

name="trackingNumber"

564

validators={{

565

onChangeListenTo: ['shippingMethod'],

566

onChange: ({ value, fieldApi }) => {

567

const shippingMethod = fieldApi.form.getFieldValue('shippingMethod');

568

if (shippingMethod === 'express' && !value) {

569

return 'Tracking number required for express shipping';

570

}

571

return undefined;

572

},

573

}}

574

>

575

{(field) => (

576

<div>

577

<input

578

value={field.state.value}

579

onChange={(e) => field.handleChange(e.target.value)}

580

/>

581

{field.state.meta.errors[0]}

582

</div>

583

)}

584

</form.Field>

585

</>

586

);

587

}

588

```

589

590

### Global Form Validation with Field Errors

591

592

```typescript

593

function OrderForm() {

594

const form = useForm({

595

defaultValues: {

596

items: [],

597

total: 0,

598

},

599

validators: {

600

onSubmit: ({ value }) => {

601

if (value.items.length === 0) {

602

return {

603

form: 'Order must have at least one item',

604

fields: {

605

items: 'Add at least one item to the order',

606

},

607

};

608

}

609

610

const calculatedTotal = value.items.reduce(

611

(sum, item) => sum + item.price,

612

0

613

);

614

615

if (calculatedTotal !== value.total) {

616

return {

617

form: 'Total does not match items',

618

fields: {

619

total: `Expected ${calculatedTotal}, got ${value.total}`,

620

},

621

};

622

}

623

624

return undefined;

625

},

626

},

627

});

628

629

return (

630

<form onSubmit={(e) => {

631

e.preventDefault();

632

form.handleSubmit();

633

}}>

634

{form.state.errors.length > 0 && (

635

<div className="form-error">

636

{form.state.errors.map((error, i) => (

637

<div key={i}>{String(error)}</div>

638

))}

639

</div>

640

)}

641

{/* fields */}

642

</form>

643

);

644

}

645

```

646