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
174 lines
7.0 KiB
Plaintext
174 lines
7.0 KiB
Plaintext
---
|
|
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()`.
|