snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
---
title: Validation
created_at: 12/19/2025
updated_at: 12/19/2025
keywords:
- validation
- validators
- schema
- records
- errors
status: published
date: 12/19/2025
readability: 8
voice: 8
completeness: 9
accuracy: 9
notes: Add more API links. Use more contractions for natural flow.
---
The `@tldraw/validate` package handles validation across tldraw's schemas, record types, and shape props. Validators enforce runtime type safety and provide structured errors when data is malformed. The `T` namespace is re-exported from `tldraw`, so `import { T } from 'tldraw'` works too.
## Where validation runs
Validation runs whenever a record is written to the [store](/sdk-features/store):
- Shape and binding props via [RecordProps](?)
- Shape, binding, and user `meta` via the `meta` field on their schema config
- Custom record types via [CustomRecordInfo](?)
- Any record type in a store via [createRecordType](?) and [StoreSchema](?)
If validation fails, the write throws and nothing is stored.
## Core validators
Use `T` validators to describe data shapes and validate unknown input:
```typescript
import { T } from '@tldraw/validate'
const userValidator = T.object({
id: T.string,
name: T.string.optional(),
age: T.number.optional(),
})
const user = userValidator.validate(input)
```
Every validator has three key methods:
- `validate(value)` validates unknown input and returns a typed result
- `isValid(value)` returns true if valid, false otherwise (useful as a type guard)
- `validateUsingKnownGoodVersion(knownGood, newValue)` reuses previously validated data to skip unchanged parts. The store calls this automatically when updating an existing record.
## Validator catalog
| Category | Validators |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| Primitives | `T.unknown`, `T.any`, `T.string`, `T.number`, `T.boolean`, `T.bigint` |
| Numbers | `T.positiveNumber`, `T.nonZeroNumber`, `T.nonZeroFiniteNumber`, `T.unitInterval`, `T.integer`, `T.positiveInteger`, `T.nonZeroInteger` |
| Collections | `T.array`, `T.arrayOf`, `T.object`, `T.unknownObject`, `T.dict`, `T.jsonDict`, `T.jsonValue` |
| Unions | `T.literal`, `T.literalEnum`, `T.setEnum`, `T.union`, `T.numberUnion`, `T.or` |
| URLs and IDs | `T.linkUrl`, `T.srcUrl`, `T.httpUrl`, `T.indexKey` |
| Modifiers | `.optional()`, `.nullable()`, `.refine()`, `.check()`, `T.optional()`, `T.nullable()`, `T.model()` |
Object validators also have `.extend()` to add fields and `.allowUnknownProperties()` to tolerate extra keys.
## Common validator patterns
```typescript
import { T } from '@tldraw/validate'
const configValidator = T.object({
id: T.string,
mode: T.literalEnum('view', 'edit'),
tags: T.arrayOf(T.string).optional(),
meta: T.object({ note: T.string }).nullable(),
})
const evenNumber = T.number.check('even', (value) => {
if (value % 2 !== 0) throw new T.ValidationError('Expected even number')
})
```
## Record props validation
Shapes and bindings use [RecordProps](?) to validate their `props` at runtime. Each key maps to a validator, and the store rejects any write whose props don't pass:
```typescript
import { DefaultColorStyle, RecordProps, T, TLBaseShape, TLDefaultColorStyle } from 'tldraw'
type CardShape = TLBaseShape<'card', { color: TLDefaultColorStyle; text: string }>
const cardShapeProps: RecordProps<CardShape> = {
color: DefaultColorStyle,
text: T.string,
}
```
Assign this object to `static override props` on your `ShapeUtil`. See the [custom shape example](/examples/shapes/tools/custom-shape) for the full util.
## Store validation and recovery
The store validates records on write. When you build your own [StoreSchema](?) you can pass `onValidationFailure` to recover or sanitize data instead of throwing:
```typescript
import { BaseRecord, RecordId, StoreSchema, createRecordType } from '@tldraw/store'
import { T, idValidator } from 'tldraw'
interface Book extends BaseRecord<'book', RecordId<Book>> {
title: string
}
const Book = createRecordType<Book>('book', {
scope: 'document',
validator: T.object({
id: idValidator<RecordId<Book>>('book'),
typeName: T.literal('book'),
title: T.string,
}),
})
const schema = StoreSchema.create(
{ book: Book },
{
onValidationFailure: (failure) => failure.record,
}
)
```
The handler must return a valid record, or rethrow to abort the write. The `failure` object is a [StoreValidationFailure](?):
| Property | Description |
| -------------- | ---------------------------------------------------------------------------------------- |
| `error` | The error that was thrown |
| `store` | The store instance where validation failed |
| `record` | The invalid record |
| `phase` | When validation failed: `'initialize'`, `'createRecord'`, `'updateRecord'`, or `'tests'` |
| `recordBefore` | The previous record state (null for new records) |
tldraw's own schema from [createTLSchema](?) (and so [createTLStore](?) and the `Tldraw` component) uses a built-in handler that reports the error and rethrows. You can't override it there; if you need recovery, build the `StoreSchema` yourself.
## Error handling
`T.ValidationError` carries structured information about what went wrong:
```typescript
import { T } from '@tldraw/validate'
const userValidator = T.object({
name: T.string,
settings: T.object({ theme: T.literalEnum('light', 'dark') }),
})
try {
userValidator.validate({ name: 'Alice', settings: { theme: 'invalid' } })
} catch (error) {
if (error instanceof T.ValidationError) {
console.log(error.message) // 'At settings.theme: Expected "light" or "dark", got invalid'
console.log(error.rawMessage) // 'Expected "light" or "dark", got invalid'
console.log(error.path) // ['settings', 'theme']
}
}
```
`rawMessage` is the message without path information, and `path` is an array showing where in the data structure validation failed (for example `['items', 0, 'name']`). The full `message` combines them.
Validators must be pure and must not mutate input values.
## Related examples
- [Custom shape](/examples/shapes/tools/custom-shape) - Define shape props with validators using RecordProps.
- [Custom validators for shape props](/examples/shapes/tools/custom-validators) - Add constraints with `.check()` and `.refine()`.