Annotations Reference
Use this page when you need the full annotation syntax, inheritance rules, merge behavior, and the built-in annotation catalog.
Annotations are metadata declarations that provide additional information about types, interfaces, and properties.
Purpose
Annotations serve multiple purposes:
- UI metadata - Labels, placeholders, descriptions for schema-driven UI tools
- Validation constraints - Min/max values, patterns, length restrictions
- Database metadata - Collection names, indexes, field strategies
- Documentation - Descriptions and multi-line documentation
- Custom metadata - Any domain-specific information
Where to Apply
Annotations can be applied anywhere:
@meta.description 'User entity' // Interface annotation
export interface User {
@meta.id // Property annotation
id: string
}
@expect.minLength 3 // Type annotation
export type Username = stringAnnotation Inheritance
Type to Property
When a property uses a type with annotations, annotations merge with property having priority:
@expect.minLength 3
@expect.maxLength 20
export type Username = string
export interface User {
@expect.maxLength 15 // Overrides type's maxLength
username: Username // Inherits minLength: 3, gets maxLength: 15
}Property References
When a property references another property, annotations merge in order:
- Final type annotations
- Referenced property annotations
- Current property annotations (highest priority)
Intersections and extends
A property several intersection members (or several parents of extends A, B) declare reads the annotations of each, the later member winning; a member written in place wins over the others. See How a merge is built.
Merge Strategies
When annotations are merged (from type inheritance, property references, or ad-hoc annotations), the merge strategy determines how same-named annotations combine:
Replace (default) — Higher-priority annotations replace lower-priority ones entirely:
@expect.min 3
export type PositiveInt = number
export interface Config {
@expect.min 10 // Replaces type's @expect.min 3
threshold: PositiveInt // Result: @expect.min is 10
}Append — Both values are kept, accumulating into an array:
@expect.pattern '^[a-z]+$'
export type SafeString = string
export interface Form {
@expect.pattern '^\S+$', 'i', 'No spaces'
code: SafeString
// Result: both patterns are validated
}The strategy is configured per annotation via AnnotationSpec. Most annotations use replace. The built-in @expect.pattern uses append.
Repeatable Annotations
Annotations marked with multiple: true can appear more than once on the same node. Their values are stored as arrays:
export interface User {
@expect.pattern '^[A-Z]'
@expect.pattern '.{3,}'
name: string
}When merged, how repeated annotations combine depends on the merge strategy:
multiple: true+ replace (default) — The higher-priority set replaces the entire arraymultiple: true+ append — Values from both sides are concatenated into a single array
Annotation Syntax
@meta.label 'User Name' // With argument
@meta.sensitive // Flag (no argument)
@expect.pattern "^[A-Z]", "i" // Multiple arguments
@meta.documentation 'Line 1' // Can be repeated
@meta.documentation 'Line 2'Arguments can be optional. Annotations without arguments are flag annotations.
String Literals
String arguments (single or double quoted) are raw: Atscript does not decode escape sequences, so the text between the quotes reaches the metadata exactly as written, backslashes included. This is why regex patterns need no double escaping:
@expect.pattern '^\S+$' // pattern is ^\S+$ (one backslash)- A backslash before the quote (
\"or\') lets the quote appear inside the string. The backslash is kept, so"say \"hi\""producessay \"hi\". - A string may end with an escaped backslash:
"C:\\"closes at its last quote and producesC:\\. An even run of backslashes before a quote ends the string; an odd run escapes the quote. \n,\tand similar sequences are not turned into control characters; they stay as two characters.
@meta.label "a\\" // value: a\\
@expect.pattern "^[^\"]+$", "u" // pattern: ^[^\"]+$Comments
Line (//) and block (/* */) comments can go anywhere in an annotation line: after the annotation or its last argument, between arguments, and on their own lines between annotations and the property. They never change the annotation's value.
@meta.id // primary key
@expect.minLength 3 /* min */, "Too short"
@meta.description 'See https://x.io' // `//` inside quotes is part of the string
id: stringA block comment that spans several lines ends the annotation like a line break, so what follows it is not an argument of that annotation.
Core Annotations
Atscript provides common-purpose annotations:
Meta Annotations (@meta.*)
@meta.label 'text'- Human-readable label@meta.id- Marks identifier field (multiple fields form composite PK)@meta.description 'text'- Field description@meta.documentation 'text'- Multi-line docs (repeatable)@meta.sensitive- Marks sensitive data@meta.readonly- Read-only field@meta.default 'value'- Default value (string as-is, other types parsed as JSON)@meta.example 'value'- Example value (string as-is, other types parsed as JSON)
UI Annotations (@ui.*)
@ui.* annotations (placeholders, form layout hints, component overrides) are provided by a separate UI plugin, not by @atscript/typescript itself. See the UI plugin docs for the full reference. The annotation shape shown in this guide (@ui.placeholder 'text', @ui.component 'name', etc.) follows the same syntax rules as the other namespaces above.
Validation Annotations (@expect.*)
@expect.minLength 5, "Custom error message"- Minimum string/array length (optional message)@expect.maxLength 100, "Custom error message"- Maximum string/array length (optional message)@expect.min 0, "Custom error message"- Minimum number value (optional message)@expect.max 100, "Custom error message"- Maximum number value (optional message)@expect.exclusiveMin 0, "Custom error message"- Number must be strictly greater than the value (optional message, since 0.1.109)@expect.exclusiveMax 1, "Custom error message"- Number must be strictly less than the value (optional message, since 0.1.109)@expect.int "Custom error message"- Must be integer (optional message)@expect.pattern "regex", "flags", "message"- Pattern validation (repeatable, optional message). Every check of a value gives the same result: thegflag has no effect, andymatches at the start of the value only (like^); the compiler warns about both@expect.minKeys 1, "Custom error message"- Object must have at least this many keys (records{ [*]: T }, objects with optional props; counts the keys the result keeps, see Key counts; optional message)@expect.maxKeys 20, "Custom error message"- Object must have at most this many keys (optional message)@expect.message "Custom error message"- Message for a type mismatch: wrong type, not the literal, no union branch, unknown discriminator, wrong tuple length (on a prop or a type; constraint failures keep their own messages). See Error Codes@expect.array.uniqueItems "Custom error message"- Enforce unique items in an array (by key fields if defined, otherwise by deep equality; optional message)@expect.array.key "Custom error message"- Mark a field as a key inside an array of objects (used for uniqueness checks, lookups, and patch operations; does not enforce uniqueness by itself; optional message)
@expect.min / @expect.max include the bound itself; @expect.exclusiveMin / @expect.exclusiveMax reject it:
interface Pricing {
@expect.exclusiveMin 0
price: number // > 0
@expect.exclusiveMin 0
@expect.exclusiveMax 1, "Rate must be below 1"
rate: number | null // null, or 0 < rate < 1
}@expect.array.key has compile-time constraints: the field must be string or number, cannot be optional, and multiple key fields form a composite key.
All validation annotations accept an optional custom error message as the last argument. When validation fails, the custom message is used instead of the default error message.
Nullable Fields
Validation annotations also work on a nullable field — a union of the target type with null / undefined, written inline or through a type alias. null passes; any other value must satisfy the constraint:
type Percent = number | null
interface Settings {
@expect.min 0
@expect.max 100
discount: number | null // null, 0..100
@expect.maxLength 64
@expect.pattern "^[a-z-]+$"
slug?: string | null // omitted, null, or a matching string ≤ 64 chars
@expect.maxLength 5
@expect.array.uniqueItems
tags: string[] | null
@expect.int
share: Percent
}A union of different non-null types (number | string) is still rejected with Expected type is (number), got union (number | string).
Array Annotations Example
@expect.array.uniqueItems and @expect.array.key work together to enforce unique array elements by identity fields:
interface Order {
@meta.id
id: number
@expect.array.uniqueItems "Duplicate line items"
items: OrderItem[]
}
interface OrderItem {
@expect.array.key
productId: number
quantity: number
price: number
}For primitive arrays, @expect.array.uniqueItems checks by deep equality — no @expect.array.key needed:
interface Product {
@expect.array.uniqueItems "Tags must be unique"
tags: string[]
}Form Validation (@meta.required)
@meta.requiredor@meta.required "Custom error message"- For strings: must contain at least one non-whitespace character. For booleans: must betrue(optional message)- On a non-optional field, a missing value (or
nullwhere the type does not accept it) fails with the@meta.requirederror and message too (codeREQUIRED,REQUIRED_CHECKEDfor booleans), not a type error. - On an optional field (
name?: string) the field may be omitted, butnullis rejected with the@meta.requiredmessage. - On a nullable field (
name: string | null)nullis accepted, and a string must still not be empty.
Form Validation
Use string.required or @meta.required to ensure required string fields are not empty or whitespace-only. A plain string type accepts '' as valid — @meta.required catches this common form validation gap. For checkboxes, @meta.required on a boolean field ensures the value is true (e.g., "accept terms").
Normalize Annotations (@normalize.*)
Normalizers change string input before it is validated:
@normalize.trim- Removes leading and trailing whitespace (also thestring.trimmedprimitive)@normalize.lowercase- Converts to lower case@normalize.uppercase- Converts to upper case
They apply in that order (trim, then lowercase, then uppercase) and only to strings: a string field, a nullable string (string | null), a union of string literals, or a type alias of one, whose fields and array items inherit them. Any other type is a compile error.
@normalize.trim
@normalize.lowercase
export type Email = string.email
export interface Signup {
email: Email // ' Ann@Example.COM ' → 'ann@example.com'
backupEmails: Email[] // each item normalized
@normalize.trim
@expect.minLength 2
name: string // checked after trimming: ' A ' fails
@normalize.lowercase
plan: 'free' | 'pro' // 'PRO' → 'pro'
}The validator never applies them. They run where input is prepared for validation: normalizeForType() (see Input Normalization) and the Moost validation pipe, which normalizes before it validates.
Emit Annotations (@emit.*)
@emit.jsonSchema— Pre-compute and embed the JSON Schema for an interface at build time, regardless of the globaljsonSchemaplugin setting@emit.compiledValidator— Emit a build-time compiled validator check for this type even when the globalcompileValidatorsoption is off. See Compiled Validators
Annotate Block Directives (@annotate.*)
@annotate.reset 'name'— Only insideannotateblocks: removes annotationname(full name, no@) from the entry's field or, at the top of the block, from the type: its own value, the one it inherits from the type it references, from anascopy's target or from lower-ranked blocks. The block's own value of that annotation applies over the reset. See Removing an Annotation.
Database Annotations (@db.*)
Database annotations (tables, columns, indexes, relations, views, schema sync) are provided by a separate package, @atscript/db/plugin, and are documented at https://db.atscript.dev. Install the DB plugin from that ecosystem to register @db.* annotations in your project.
Special Annotation Argument Types
Some annotations accept special argument types beyond strings and numbers:
- Ref arguments — Type references using dot-notation chains (e.g.,
User.id). Custom annotations can declaretype: 'ref'arguments. - Query arguments — SQL-like expressions in backticks (e.g.,
`Task.status != 'done'`). Custom annotations can declaretype: 'query'arguments. Comparison operators (=,!=,>,>=,<,<=) work at any nesting level, including inside parentheses — e.g.`A.at >= B.start and (B.end = null or A.at <= B.end)`(since 0.1.90). - Literal value arguments — custom annotations can declare
type: 'value': a string, number or boolean literal stored as written (@x.default 0is the number0); a number or boolean must fit the field's type. See Literal Values. - Expression and order arguments (since 0.1.99) — custom annotations can declare
type: 'expr'(arithmetic:`openCount * 10 + coalesce(rank, 0)`) ortype: 'order'(`raisedAt desc, id`) backtick arguments. Inside backticks/is division; a regexp literal is recognized only aftermatches. See Expression and Order Arguments.