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
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:
173
apps/docs/content/sdk-features/validation.mdx
Normal file
173
apps/docs/content/sdk-features/validation.mdx
Normal 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()`.
|
||||
Reference in New Issue
Block a user