Skip to content

Code Generation ​

The TypeScript plugin generates code from .as files in two formats: .d.ts for type checking and .js for runtime metadata.

DTS Format ​

The default format. Generates TypeScript declaration files for static type checking and IDE support.

Given this .as file:

atscript
@meta.description 'A product in the catalog'
export interface Product {
    @meta.label 'Product Name'
    @expect.minLength 3
    name: string

    @expect.min 0
    price: number

    inStock: boolean
}

The generated .d.ts:

typescript
export declare class Product {
  name: string
  price: number
  inStock: boolean
  static __is_atscript_annotated_type: true
  static type: TAtscriptTypeObject<keyof Product, Product>
  static metadata: TMetadataMap<AtscriptMetadata>
  static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof Product>
  static toJsonSchema: () => any
  static toExampleData?: () => any
}
export declare namespace Product {
  type DataType = Product
}

Key points:

  • Interfaces become declare class with instance properties and static members
  • The static members provide access to type metadata, validation, JSON Schema, and example data at runtime
  • toExampleData is always optional — when exampleData: true is set in plugin options, it's rendered without deprecation; otherwise it's marked @deprecated
  • Product.DataType is a type alias for the data shape — useful for generic utilities

DB entity statics ​

Interfaces annotated with @db.table, @db.view or @db.view.for additionally get static __flat and static __ownProps — maps from every dot-notation path ("address.zip") to its value type, which the DB layer uses to type filters, $select and $sort. Intermediate paths are never and @db.json fields are string; since 0.1.93 optional entries are typed <type> | null ("address.zip"?: string | null) because the DB layer treats optional as nullable in filters — the instance properties keep their plain zip?: string shape. A recursive type is listed down to where it repeats: a property leading back to a type already expanded on its path is listed as never, and nothing below it (Category.parent in interface Category { parent?: Category }). Earlier versions listed some paths below such a property (children.parent.id); filters on them no longer type-check. A table whose flattening reaches mutually recursive merges is a compile error.

Interface Extends ​

When an interface uses extends, the first parent becomes the TypeScript extends target; properties from additional parents and own properties are merged into the class body. At runtime an inherited prop is the parent's own prop node, and a prop several parents declare reads through all of theirs (see Interface Extends), so metadata.get(...) works on every inherited prop.

For types (not interfaces), a companion namespace carries the same __is_atscript_annotated_type, type, metadata, validator, toJsonSchema, and toExampleData static members.

Type paths ​

A type path to a scalar field is written as that scalar (customerId: string /* id */). Any other path is a chain of TAtscriptField steps, a type-only export of @atscript/typescript/utils:

typescript
// office?: Account.job.office   (type Account = User | null; office: Address)
office?: TAtscriptField<TAtscriptField<Account, "job">, "office">

TAtscriptField<T, K> is T[K] for each object branch of T that declares K, and nothing for the other branches (null, scalars, arrays, objects without K; a key that only pattern properties match is not a declared property). So a path through a union is the union of its fields, and a step through an optional property (work?: Job) does not index undefined. Versions before 0.2.0 wrote Account["job"]["office"], which failed to type-check through a union or an optional property.

The data type and the runtime descriptor of a type alias of a path can differ. Fields of one type are one TypeScript type (extends and merges work on it), but the runtime keeps one item per field, so the descriptor (Z.type) is typed as the union the runtime holds:

typescript
// type Z = Place.address   (type Place = Office | Home, both `address: Address`)
export type Z = TAtscriptField<Place, 'address'> // Address
declare namespace Z {
  const type: TAtscriptTypeComplex<Z> // a union: one item per field
}

A path naming several phantom fields is one phantom node at runtime, and its descriptor is typed as that node (TAtscriptTypeFinal).

JS Format ​

Generates JavaScript files with full runtime metadata. Use this when you need validation, metadata access, or serialization.

The generated .js registers the type tree at module load via internal builder calls — you should not need to read or hand-write it. The .d.ts is the consumer-facing surface; the .js exists to make the static members on each generated class actually work at runtime. Set jsonSchema and exampleData in plugin options to control what runtime helpers get pulled in, and compileValidators to add build-time validator checks.

When to Use Which ​

.d.ts.js
Type checkingYesYes (with .d.ts alongside)
IDE IntelliSenseYesYes
Runtime validationNeeds .jsYes
Metadata accessNeeds .jsYes
JSON SchemaNeeds .jsYes
Bundle sizeZero (types only)Includes runtime metadata

In practice, use .d.ts during development (generated by VSCode extension on save) and .js for builds that need runtime features.

Next Steps ​

  • CLI — build .as files from the command line
  • Type Definitions — understand the generated type structure
  • Validation — validate data against generated types

Released under the MIT License.