UNPKG

@tldraw/tlschema

Version:

tldraw infinite canvas SDK (schema).

683 lines (530 loc) • 16.4 kB
# @tldraw/tlschema The schema package defines the type system, data structures, validation, and migrations for tldraw's persisted data. It provides a complete, type-safe, and version-aware data model that powers the tldraw editor. ## 1. Introduction **@tldraw/tlschema** is the foundational package that defines how tldraw stores and manages data. It contains: - **Record types** for all persisted data (shapes, assets, pages, user state) - **Validation schemas** that ensure data integrity at runtime - **Migration sequences** that handle data evolution over time - **Style properties** that enable consistent styling across shapes You'll use this package when creating custom shapes, defining your own data schemas, or when you need to work with tldraw's data structures directly. ## 2. Core Concepts ### Schemas: The Foundation A **schema** defines the structure of your tldraw store. It specifies what types of records can exist, how they're validated, and how they evolve over time. ```ts import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema' // Create a schema with default shapes const schema = createTLSchema({ shapes: defaultShapeSchemas, }) ``` ### Records: The Data Units **Records** are the individual pieces of data stored in tldraw. Every record has a type, an ID, and properties specific to that type: ```ts import { TLShape, TLPage, TLAsset } from '@tldraw/tlschema' // All records extend BaseRecord const shape: TLShape = { id: 'shape:abc123', typeName: 'shape', type: 'geo', x: 100, y: 200, rotation: 0, // ... other properties } ``` ### Style Properties: Shared Styling **Style properties** are special properties that can be applied across multiple shapes and persist for new shape creation: ```ts import { StyleProp } from '@tldraw/tlschema' import { T } from '@tldraw/validate' // Define a custom style property const MyCustomStyle = StyleProp.define('myapp:custom', { defaultValue: 'default', type: T.string, }) ``` ## 3. Basic Usage ### Creating a Custom Schema You create schemas by combining shape configurations, bindings, and migrations: ```ts import { createTLSchema, defaultShapeSchemas, defaultBindingSchemas } from '@tldraw/tlschema' const schema = createTLSchema({ shapes: { ...defaultShapeSchemas, // Add custom shapes here }, bindings: defaultBindingSchemas, }) ``` ### Working with Shape Records Shape records are the most common type you'll work with. Every shape extends `TLBaseShape`: ```ts import { createShapeValidator } from '@tldraw/tlschema' import { T } from '@tldraw/validate' const CUSTOM_TYPE = 'custom' // Define a custom shape type declare module '@tldraw/tlschema' { export interface TLGlobalShapePropsMap { [CUSTOM_TYPE]: MyCustomShapeProps } } interface MyCustomShapeProps { width: number height: number color: string } type MyCustomShape = TLShape<typeof CUSTOM_TYPE> // Create validation for your shape const customShapeValidator = createShapeValidator(CUSTOM_TYPE, { width: T.number, height: T.number, color: T.string, }) ``` ### Integrating with Store Once you have a schema, you use it to create a store: ```ts import { Store } from '@tldraw/store' import { TLStoreProps } from '@tldraw/tlschema' const store = new Store({ schema, props: { defaultName: 'My Drawing', assets: myAssetStore, // Your asset storage implementation }, }) ``` ## 4. Advanced Topics ### Creating Custom Shapes When creating custom shapes, follow this pattern for complete integration: ```ts import { createShapeValidator, createShapePropsMigrationSequence, RecordProps, } from '@tldraw/tlschema' import { DefaultColorStyle } from '@tldraw/tlschema' const MY_SHAPE_TYPE = 'myshape' // 1. Define the shape declare module '@tldraw/tlschema' { export interface TLGlobalShapePropsMap { [MY_SHAPE_TYPE]: MyShapeProps } } interface MyShapeProps { color: typeof DefaultColorStyle // Use existing style width: number height: number customData: string } type MyShape = TLShape<typeof MY_SHAPE_TYPE> // 2. Create props validation const myShapeProps: RecordProps<MyShape> = { color: DefaultColorStyle, width: T.number, height: T.number, customData: T.string, } // 3. Define migrations for schema evolution const myShapeMigrations = createShapePropsMigrationSequence({ sequenceId: 'com.myapp.shape.myshape', sequence: [ { id: 'com.myapp.shape.myshape/1.0.0', up: (props) => props, // Initial version down: (props) => props, }, ], }) // 4. Add to schema const schema = createTLSchema({ shapes: { ...defaultShapeSchemas, myshape: { props: myShapeProps, migrations: myShapeMigrations, }, }, }) ``` ### Custom Style Properties Style properties enable consistent styling across shapes and remember the last used value: ```ts import { StyleProp, EnumStyleProp } from '@tldraw/tlschema' import { T } from '@tldraw/validate' // Free-form style property const MyWidthStyle = StyleProp.define('myapp:width', { defaultValue: 2, type: T.number, }) // Enum-based style property const MyPatternStyle = StyleProp.defineEnum('myapp:pattern', { defaultValue: 'solid', values: ['solid', 'dashed', 'dotted'], }) // Use in shape props interface MyShapeProps { width: typeof MyWidthStyle pattern: typeof MyPatternStyle // other props... } ``` ### Asset Management Assets represent external resources like images, videos, or bookmarks: ```ts import { TLImageAsset, TLAssetStore } from '@tldraw/tlschema' // Implement asset storage const assetStore: TLAssetStore = { async upload(asset, file) { // Upload file to your storage service const src = await uploadToStorage(file) return { src } }, async resolve(asset, context) { // Resolve asset URL for rendering return asset.props.src }, async remove(assetIds) { // Clean up removed assets await deleteFromStorage(assetIds) }, } ``` ### Migration Strategies Migrations handle schema evolution as your application develops: ```ts import { createShapePropsMigrationSequence } from '@tldraw/tlschema' const migrations = createShapePropsMigrationSequence({ sequenceId: 'com.myapp.shape.custom', sequence: [ { id: 'com.myapp.shape.custom/1.1.0', up: (props) => { // Add new property with default value return { ...props, newProperty: 'default' } }, down: ({ newProperty, ...props }) => { // Remove property for backward compatibility return props }, }, { id: 'com.myapp.shape.custom/1.2.0', up: (props) => { // Rename property return { ...props, renamedProperty: props.oldProperty, oldProperty: undefined, } }, down: (props) => { return { ...props, oldProperty: props.renamedProperty, renamedProperty: undefined, } }, }, ], }) ``` ## 5. Built-in Shape Types ### Default Shapes tldraw includes several built-in shape types: ```ts import { TLGeoShape, // Rectangles, ellipses, triangles, etc. TLTextShape, // Text with rich formatting TLDrawShape, // Freehand drawing paths TLArrowShape, // Arrows with optional binding to shapes TLLineShape, // Multi-point lines and splines TLImageShape, // Raster images TLVideoShape, // Video files TLNoteShape, // Sticky notes TLBookmarkShape, // Website bookmarks TLEmbedShape, // Embedded content (YouTube, Figma, etc.) TLFrameShape, // Frames for grouping content TLGroupShape, // Groups for organizing shapes TLHighlightShape, // Highlighting tool strokes } from '@tldraw/tlschema' ``` ### Shape Properties All shapes share common base properties from `TLBaseShape`: ```ts interface TLBaseShape<Type, Props> { id: TLShapeId type: Type x: number // Position X y: number // Position Y rotation: number // Rotation in radians index: IndexKey // Fractional index for ordering parentId: TLParentId // Parent page or shape isLocked: boolean // Whether shape can be selected opacity: TLOpacityType // Transparency (0-1) props: Props // Shape-specific properties meta: JsonObject // User-defined metadata } ``` ### Geo Shapes Geometric shapes support various styles and configurations: ```ts import { TLGeoShape, GeoShapeGeoStyle } from '@tldraw/tlschema' // Geo shapes can be rectangles, ellipses, triangles, etc. const geoShape: TLGeoShape = { // ... base properties props: { geo: GeoShapeGeoStyle, // 'rectangle', 'ellipse', 'triangle', etc. w: 100, // Width h: 80, // Height color: 'blue', // Color style fill: 'solid', // Fill style dash: 'solid', // Dash style size: 'm', // Size style richText: null, // Optional text content }, } ``` ## 6. Validation and Type Safety ### Runtime Validation All records are validated at runtime to ensure data integrity: ```ts import { T } from '@tldraw/validate' import { createShapeValidator } from '@tldraw/tlschema' // Validation happens automatically when records enter the store const validator = createShapeValidator('myshape', { width: T.number.check((n) => n > 0), // Custom validation height: T.number.check((n) => n > 0), color: T.string, }) ``` ### Error Handling When validation fails, you can handle errors gracefully: ```ts try { store.put([invalidRecord]) } catch (error) { if (error instanceof ValidationError) { console.log('Validation failed:', error.message) // Handle validation error appropriately } } ``` ### Type-Safe IDs Record IDs are strongly typed to prevent mixing different record types: ```ts import { TLShapeId, TLPageId, createShapeId } from '@tldraw/tlschema' // IDs are branded types - compiler prevents mixing them up const shapeId: TLShapeId = createShapeId() const pageId: TLPageId = 'page:123' // TypeScript error if you use wrong format ``` ## 7. Integration with @tldraw/store ### Store Configuration The schema integrates with the store system to provide reactive data management: ```ts import { Store } from '@tldraw/store' import { TLStoreProps, createTLSchema } from '@tldraw/tlschema' const schema = createTLSchema() const store = new Store({ schema, props: { defaultName: 'Untitled', assets: assetStore, onMount: (editor) => { // Initialize when editor mounts console.log('Editor mounted with store') }, }, }) ``` ### Reactive Queries The store provides reactive access to records: ```ts import { track } from '@tldraw/state' // This function will re-run when shapes change const ShapeCounter = track(() => { const shapes = store.query.records('shape').get() return `Total shapes: ${shapes.length}` }) ``` ## 8. Performance Considerations ### Validation Optimization - Use `validateUsingKnownGoodVersion()` when you know data is already valid - Minimize validation in hot paths during user interactions - Consider validation levels (development vs production) ### Memory Management - Style properties are shared across shapes to reduce memory usage - Records use immutable structures to prevent accidental mutations - `devFreeze()` helps catch mutation bugs in development ### Migration Performance - Group related changes into single migration steps - Test migration performance with realistic data sizes - Use migration sequence IDs to optimize dependency resolution ## 9. Debugging ### Understanding Schema Structure You can inspect your schema to understand its configuration: ```ts const schema = createTLSchema() // Examine record types console.log('Record types:', Object.keys(schema.types)) // Check validation for specific record type const shapeValidator = schema.types.shape console.log('Shape validator:', shapeValidator) ``` ### Migration Debugging When migrations fail, examine the migration sequence: ```ts // Check migration history const migrations = schema.sortedMigrations console.log( 'Migration sequence:', migrations.map((m) => m.id) ) // Test individual migrations try { const migrated = migrator.migrateStoreSnapshot({ schema: oldSchema, store: snapshot, }) console.log('Migration successful') } catch (error) { console.error('Migration failed:', error) } ``` ### Validation Errors When records fail validation, examine the validation path: ```ts import { T } from '@tldraw/validate' try { shapeValidator.validate(invalidShape) } catch (error) { console.log('Validation path:', error.path) console.log('Validation message:', error.message) console.log('Invalid value:', error.value) } ``` ## 10. Common Patterns ### Shape with Asset References Many shapes reference assets for their content: ```ts interface MediaShapeProps { assetId: TLAssetId | null width: number height: number crop?: TLShapeCrop // Optional cropping info } // Usage pattern const imageShape: TLImageShape = { // ... base properties props: { assetId: 'asset:image123', w: 200, h: 150, crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 }, }, }, } ``` ### Binding Relationships Arrows can bind to other shapes: ```ts import { TLArrowBinding } from '@tldraw/tlschema' const arrowBinding: TLArrowBinding = { id: 'binding:abc123', typeName: 'binding', type: 'arrow', fromId: 'shape:arrow1', // Arrow shape ID toId: 'shape:rectangle1', // Target shape ID props: { terminal: 'end', // 'start' or 'end' normalizedAnchor: { x: 0.5, y: 0.5 }, // Position on target isExact: false, // Whether position is exact isPrecise: true, // Whether binding is precise }, } ``` ### Rich Text Content Shapes can contain formatted text: ```ts import { TLRichText, toRichText } from '@tldraw/tlschema' const richText: TLRichText = toRichText('Hello **bold** text') const textShape: TLTextShape = { // ... base properties props: { color: 'black', size: 'm', font: 'draw', textAlign: 'start', richText: richText, autoSize: true, scale: 1, }, } ``` ## 11. Best Practices ### Schema Design - **Start simple** - Begin with minimal shape properties and add complexity gradually - **Use existing styles** - Leverage built-in style properties before creating custom ones - **Plan for evolution** - Design your schema with migrations in mind - **Validate thoroughly** - Include validation for edge cases and invalid states ### Migration Strategy - **Version incrementally** - Use semantic versioning in migration IDs - **Test both directions** - Ensure up and down migrations work correctly - **Handle failures gracefully** - Provide fallbacks when migrations fail - **Document breaking changes** - Clearly communicate migration requirements ### Performance Optimization - **Minimize validation overhead** - Use efficient validators for frequently accessed properties - **Batch related changes** - Group property updates to reduce reactive updates - **Use appropriate record scopes** - Choose correct scope (document/session/presence) for data - **Optimize asset handling** - Implement efficient asset storage and resolution ### Error Handling - **Validate early** - Catch validation errors as close to the source as possible - **Provide clear messages** - Use descriptive error messages for debugging - **Handle partial failures** - Design systems that can recover from individual record failures - **Log strategically** - Include enough context for debugging without overwhelming logs ## 12. Extension Points ### Custom Shapes Extend the built-in shape system: ```ts // Add custom shapes to default schemas const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, myCustomShape: customShapeConfig, }, }) ``` ### Custom Bindings Create new types of shape relationships: ```ts const CUSTOM_TYPE = 'custom' // Define custom binding types declare module '@tldraw/tlschema' { export interface TLGlobalBindingPropsMap { [CUSTOM_TYPE]: MyBindingProps } } type MyCustomBinding = TLBinding<typeof CUSTOM_TYPE> const customBindingConfig = { props: myBindingProps, migrations: myBindingMigrations, } ``` ### Asset Storage Implement custom asset storage backends: ```ts const customAssetStore: TLAssetStore = { async upload(asset, file) { return await myCloudStorage.upload(file) }, async resolve(asset, context) { return await myCloudStorage.getUrl(asset.props.src, context) }, async remove(assetIds) { await Promise.all(assetIds.map((id) => myCloudStorage.delete(id))) }, } ``` The tlschema package provides a robust foundation for building applications with tldraw. By following these patterns and understanding the core concepts, you can create custom shapes, manage data effectively, and build experiences that scale with your users' needs.