Back to Tldraw

Validation

apps/docs/content/sdk-features/validation.mdx

5.4.06.7 KB
Original Source

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:

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

CategoryValidators
PrimitivesT.unknown, T.any, T.string, T.number, T.boolean, T.bigint
NumbersT.positiveNumber, T.nonZeroNumber, T.nonZeroFiniteNumber, T.unitInterval, T.integer, T.positiveInteger, T.nonZeroInteger
CollectionsT.array, T.arrayOf, T.object, T.unknownObject, T.dict, T.jsonDict, T.jsonValue
UnionsT.literal, T.literalEnum, T.setEnum, T.union, T.numberUnion, T.or
URLs and IDsT.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 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:

PropertyDescription
errorThe error that was thrown
storeThe store instance where validation failed
recordThe invalid record
phaseWhen validation failed: 'initialize', 'createRecord', 'updateRecord', or 'tests'
recordBeforeThe 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.