Primitives
Primitives are the building blocks of every .as model. In day-to-day app code, the main thing to learn is that Atscript lets you refine primitives with semantic extensions like string.email and number.int.
Basic Primitive Types
Atscript supports the following primitive types:
string- Text valuesnumber- Numeric valuesdecimal- Decimal number stored as string to preserve precisionboolean- True/false valuesnull- Null valueundefined- Undefined valuevoid- No valueunknown- Any value (seeunknownbelow)
export interface BasicTypes {
text: string
count: number
price: decimal
isEnabled: boolean
empty: null
nothing: void
}Semantic Types
Primitives can be extended with dot notation:
export interface User {
email: string.email
name: string.required
age: number.int.positive
}These semantic types do two useful things:
- they make the model easier to read
- they attach validation behavior automatically
String Extensions
The most common string extensions are:
export interface User {
id: string.uuid
email: string.email
phone: string.phone
name: string.required
birthDate: string.date
createdAt: string.isoDate
website: string.url
serverIp: string.ip
initial: string.char
}Use them when the field has a real meaning that is stronger than plain string.
Other string extensions include string.ipv4 and string.ipv6 for protocol-specific IP address validation, and string.trimmed: a string whose surrounding whitespace is removed before validation (the @normalize.trim normalizer).
Number Extensions
The most common number extensions are:
export interface Product {
quantity: number.int
price: number.positive
discount: number.negative
weight: number.double
}Useful built-ins:
number.int— integer onlynumber.positive— minimum0(inclusive:0passes; add@expect.exclusiveMin 0to reject it)number.negative— maximum0(inclusive:0passes; add@expect.exclusiveMax 0to reject it)number.single/number.double— numeric intent tags
Sized Integer Types
For fields that need a specific bit width, number.int provides sized extensions:
export interface SensorData {
reading: number.int.int16
flags: number.int.uint8
port: number.int.uint16.port
offset: number.int.int32
}Signed: int8, int16, int32, int64. Unsigned: uint8, uint16, uint32, uint64. Aliases: uint8.byte (byte value), uint16.port (network port).
Boolean Extensions
Boolean extensions are mostly useful for required checkboxes and flags:
export interface Settings {
agreed: boolean.required
alwaysOn: boolean.true
disabled: boolean.false
}boolean.required is the one most application code needs. It means the value must be true.
Combining Extensions
You can combine extensions when the field needs more than one rule:
export interface Metrics {
retries: number.int.positive
loss: number.double.negative
}Prefer semantic types over separate @expect.* annotations when a built-in semantic type already says what you mean.
Decimal Type
Use decimal for fields that need exact decimal precision — prices, financial amounts, measurements:
export interface Product {
@db.column.precision 10, 2
price: decimal
@db.column.precision 8, 4
exchangeRate: decimal
}At runtime, decimal values are strings (e.g., "19.99"). This preserves precision and survives JSON transport without any loss — unlike floating-point number, which can introduce rounding errors for decimal fractions.
In SQL databases, decimal maps to the native DECIMAL type (with precision/scale from @db.column.precision). Use number for general-purpose numerics and decimal when exact decimal representation matters.
Advanced Primitives
Timestamp Variants
Atscript provides timestamp-oriented numeric tags: number.timestamp, number.timestamp.created, and number.timestamp.updated.
number.timestamp.created also carries @db.default.now, so a database layer fills it with the insert time. number.timestamp.updated carries @db.default.now and @db.onUpdate.now, so a database layer fills it with the insert time and sets it to the current time on every update. Both apply to fields typed with them directly or through a type alias, not to a field that references another one (createdAt: Order.createdAt). With @atscript/db, number.timestamp.updated is set on every write from atscript 0.1.106 / @atscript/db 0.1.156 — earlier releases left it unset.
Timestamps are stored as number and constrained to integers — the unit (seconds, milliseconds, microseconds) is project-decided. The primitive itself does not enforce a unit. Storing timestamps as numbers is a deliberate choice — numbers are JSON-native, so timestamps pass through HTTP boundaries (client ↔ server) without any serialization or hydration step. Using Date objects would require walking every response to convert strings back to Date instances on both sides of the transport layer.
These are advanced because they matter more for DB integrations than for basic TypeScript usage.
unknown
unknown is a field that holds any value: JSON data of any shape you don't model (a settings blob, a provider's raw payload).
export interface Event {
name: string
payload: unknown
meta?: unknown
}- TypeScript:
unknown. - Validation: any value passes,
nulland nested objects included; nothing inside it is checked.undefinedis never a value: a requiredunknownfield must be present, and anunknown[]element can't beundefined. An optionalunknownfield may be omitted. - Default data:
createDataFromAnnotatedTypegives a requiredunknownfieldnull. - JSON Schema:
{}.
unknown merges with a scalar like any scalar: { v: unknown } & { v: string } gives v: string. Merging it with an object, an array or null (unknown & { x: string }) is a compile error: write the other type alone.
unknown is data. phantom, below, is the opposite: it is not part of the data at all.
phantom
phantom is a special non-data type for runtime-discoverable elements that should not appear in TypeScript data, validation, or JSON Schema.
It is useful for advanced UI tooling and type traversal, but most application code can ignore it until needed.
Best Practices
- Use semantic types when the field has real meaning beyond a plain primitive.
- Let built-in semantic types carry validation instead of duplicating the same rule by hand.
- Reach for
string.required,string.email, andnumber.intearly — they cover many common cases. - Use
decimalinstead ofnumberwhen exact decimal precision matters (prices, financial data). - Save advanced primitives like
phantomand timestamp variants for pages or features that really need them.
Combining Extensions
number.int.positive— positive integers onlynumber.double.negative— negative double-precision numbersnumber.single.positive— positive single-precision numbersnumber.int.uint16.port— network port number
Type Tags in TypeScript
When compiled, semantic types preserve their extensions as tags:
// Generated TypeScript
export declare class User {
email: string /* email */
age: number /* int */
// ...
}
// Runtime metadata access
const emailProp = User.type.props.get('email')
const emailTags = emailProp?.type.tags // ['email', 'string']
const ageProp = User.type.props.get('age')
const ageTags = ageProp?.type.tags // ['int', 'number']These tags can be used by:
- Validators to apply appropriate rules
- UI generators to choose correct input types
- API documentation generators
- Database schema generators
DOs and DON'Ts
- DO treat semantic types as format checks, not semantic truth. They validate with regex patterns:
string.dateaccepts99/99/9999(it matches theMM/DD/YYYYshape), andstring.emailuses a pragmatic pattern, not full RFC 5322. Add application-level checks when calendar validity or deliverability actually matters. - DON'T use
@expect.minLength 1when you meanstring.required.string.requiredrejects whitespace-only strings like" ";minLength 1accepts them. - DON'T stack
@expect.patternon a semantic type to broaden it. Patterns are conjunctive — every pattern must match — so an extra pattern onstring.emailonly narrows it further. For alternative formats, define a custom primitive with a single alternation regex (^(?:formA|formB)$).
Next Steps
- Annotations — add metadata to your types
- Custom Primitives — define your own primitive extensions
- Type Definitions — how tags and metadata are accessed at runtime