Custom Primitives
You can extend the built-in primitive types with your own semantic extensions via atscript.config.ts. Custom primitives work exactly like built-in ones — they appear in IntelliSense, carry validation constraints, and generate appropriate type tags. For the full primitive system (complex shapes, container primitives, semantic tags, phantom design), see Custom Primitives — Plugin Development.
Quick Example
Add custom extensions under the primitives key in your config:
import { defineConfig } from '@atscript/core'
import ts from '@atscript/typescript'
export default defineConfig({
rootDir: 'src',
plugins: [ts()],
primitives: {
string: {
extensions: {
url: {
type: 'string',
documentation: 'URL format',
annotations: {
'expect.pattern': {
pattern: '^https?://.+$',
message: 'Invalid URL',
},
},
},
slug: {
type: 'string',
documentation: 'URL-safe slug',
annotations: {
'expect.pattern': {
pattern: '^[a-z0-9-]+$',
message: 'Invalid slug',
},
},
},
},
},
number: {
extensions: {
percentage: {
type: 'number',
documentation: 'Percentage value (0–100)',
annotations: {
'expect.min': 0,
'expect.max': 100,
},
},
},
},
},
})Then use them in .as files with dot notation:
export interface Page {
url: string.url
slug: string.slug
completeness: number.percentage
}The validator will automatically enforce the constraints — no @expect.* annotations needed.
What You Can Define
Each primitive extension supports:
| Field | Description |
|---|---|
type | The base type ('string', 'number', 'boolean', 'phantom', etc.) — inherited from parent if omitted |
documentation | Description shown in IntelliSense — inherited from parent if omitted |
extensions | Nested sub-extensions (e.g., number.int.positive) |
isContainer | If true, the primitive cannot be used directly — one of its extensions must be chosen |
tags | Array of semantic tags (e.g., ['created']) — inherited from parent, used by DB adapters and runtime tools |
annotations | Implicit annotations applied to any field using this primitive (e.g., { 'expect.int': true, 'expect.min': 0 }) — merged with parent's annotations |
isContainer
When isContainer: true, the primitive itself cannot be used as a type — only its extensions are valid:
field: ui // ✗ Error — ui is a container, must use an extension
field: ui.action // ✓ Correct — uses the extensionInheritance
Extensions automatically inherit type, documentation, annotations, and tags from their parent primitive. You only need to specify fields you want to override or add. This is how built-in extensions like string.email work — they inherit type: 'string' from string and only add their own expect.pattern annotation.
Phantom Namespaces
You can define entirely new primitive namespaces with type: 'phantom' to create families of non-data UI elements. These are omitted from TypeScript types and skipped by validation, but discoverable at runtime — perfect for form renderers.
primitives: {
ui: {
type: 'phantom',
isContainer: true,
documentation: 'Non-data UI elements for form rendering',
extensions: {
action: { documentation: 'An action element (button, link)' },
divider: { documentation: 'A visual divider between form sections' },
paragraph: { documentation: 'A block of informational text' },
},
},
}export interface CheckoutForm {
@meta.label 'Email'
email: string.email
@meta.label 'Shipping Address'
shippingHeader: ui.divider
@meta.label 'Street'
street: string
}Annotations are always namespaced — use @meta.label (built-in), not bare @label. Custom annotations follow the same rule (@grid.column, @form.section, etc.).
Next Steps
- Primitives — built-in primitive types and semantic extensions
- Custom Annotations — define custom annotation types
- Configuration — full config file reference