Metadata
All annotations from .as files are preserved as runtime metadata on generated types. This is what makes Atscript more than just a type system — annotations like labels, descriptions, and validation rules are accessible in your TypeScript code.
Accessing Metadata
Every generated type has a metadata field — a typed Map keyed by annotation names:
import { Product } from './product.as'
// Top-level annotations (on the interface itself)
Product.metadata.get('meta.description') // 'A product in the catalog'
// Property-level annotations
const nameProp = Product.type.props.get('name')
nameProp?.metadata.get('meta.label') // 'Product Name'
nameProp?.metadata.get('expect.minLength') // { length: 3, message?: string }2
3
4
5
6
7
8
9
The metadata map is typed via the global AtscriptMetadata interface, so you get IntelliSense for annotation keys and their value types.
A map returns the annotations in effect at that node: its own, plus those it reads from the type it refers to (a property typed Address has Address's annotations under its own). Treat the maps as read-only; to change a type, write an annotate block. Writing at runtime is covered in Runtime Metadata.
Iterating Properties
Walk all properties and their metadata using the props map:
import { Product } from './product.as'
for (const [key, prop] of Product.type.props.entries()) {
const label = prop.metadata.get('meta.label') || key
const required = !prop.optional
console.log(`${label} (${key}): ${required ? 'required' : 'optional'}`)
}2
3
4
5
6
7
Tags
Semantic types like string.email or number.positive produce tags that are available at runtime:
import { User } from './user.as'
const emailProp = User.type.props.get('email')
emailProp?.type.tags // Set { 'email', 'string' }2
3
4
Tags let you make runtime decisions based on semantic types — for example, rendering an email input vs a plain text input.
Nested Types
Every nested node — array element, union member, nested-object property — is itself a TAtscriptAnnotatedType with its own .metadata.get(...). Reach into the right field on the parent (type.of for arrays, type.items for unions/intersections/tuples, type.props.get(...) for objects) and call metadata.get on what you find:
import { User } from './models.as'
const addressProp = User.type.props.get('address')
if (addressProp?.type.kind === 'object') {
const cityProp = addressProp.type.props.get('city')
cityProp?.metadata.get('meta.label')
}2
3
4
5
6
7
Use forAnnotatedType() to traverse nested types generically — see Type Traversal for recursive walking patterns and practical examples.
Practical Example: Build A Field List
import { Product } from './product.as'
function buildFieldList() {
if (Product.type.kind !== 'object') return []
return Array.from(Product.type.props.entries()).map(([key, prop]) => ({
name: key,
label: prop.metadata.get('meta.label') || key,
required: !prop.optional,
placeholder: prop.metadata.get('ui.placeholder'),
sensitive: prop.metadata.get('meta.sensitive') || false,
readonly: prop.metadata.get('meta.readonly') || false,
}))
}
const fields = buildFieldList()
// [{ name: 'name', label: 'Product Name', required: true, ... }, ...]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
For more advanced traversal patterns — recursive walking, flattening nested types, collecting metadata across the type tree — see Type Traversal.
Next Steps
- Validation — annotations drive validation rules automatically
- Type Definitions — the annotated type system in depth
- Serialization — serialize types with metadata for transport