#Type Safety (The t Object)
In many full-stack tools, you end up defining your data structures three times:
- Once for the Database (SQL/ORM)
- Once for the API Validation (Zod/Joi)
- Once for TypeScript interfaces
If you change one, you have to change the others. It's exhausting and error-prone.
Alepha solves this with the t object.
#One Schema to Rule Them All
Alepha uses TypeBox as its schema definition language, wrapped as t. We chose TypeBox because it compiles down to standard JSON Schema, provides compile-time type inference without code generation, and is ridiculously fast.
When you define an object with t, Alepha uses it for everything:
1const userSchema = t.object({2 username: t.text(),3 age: t.integer()4});
- Runtime Validation: Used by
$actionto validate incoming HTTP JSON bodies. - Database Definition: Used by
$entityto generateCREATE TABLEstatements. - TypeScript Inference: Used by your IDE to give you autocomplete.
- Documentation: Used to generate OpenAPI/Swagger specs.
#Static Type Inference
You don't need to write TypeScript interfaces. Use Static<T> to extract the type:
1import { t, type Static } from "alepha"; 2 3const userSchema = t.object({ 4 id: t.uuid(), 5 email: t.email(), 6 age: t.integer(), 7}); 8 9type User = Static<typeof userSchema>;10// { id: string; email: string; age: number }
#Strings
#t.string() vs t.text()
t.string() is raw and unbounded. Use it for internal stuff.
t.text() is the safe version: max 255 chars by default, auto-trims whitespace. Use it for user input.
1t.string() // any string, no limits2t.text() // max 255, trimmed3t.text({ maxLength: 1000 }) // custom limit4t.text({ trim: false }) // keep whitespace5t.text({ lowercase: true }) // force lowercase
#Size Helpers
1t.shortText() // max 64 - names, titles2t.text() // max 255 - default3t.longText() // max 1024 - descriptions4t.richText() // max 65535 - HTML, Markdown
Same as t.text({ size: "short" }), etc.
#Numbers
1t.number() // any number2t.number({ minimum: 0 }) // non-negative3t.integer() // whole numbers4t.integer({ minimum: 1 }) // positive integers5t.int32() // -2B to 2B (Postgres INT4)6t.int64() // safe JS integer range7t.bigint() // string representation for true 64-bit
t.int64() is not a real int64. JavaScript can't represent all 64-bit integers. If you need the full range, use t.bigint() which stores it as a string like "123456789012345678".
#Booleans
1t.boolean()
Nothing fancy here.
#Format Types
1t.uuid() // "550e8400-e29b-41d4-a716-446655440000"2t.email() // auto-trims and lowercases3t.url() // "https://example.com"4t.e164() // "+1234567890" (phone)5t.bcp47() // "en", "en-US", "fr-CA" (language tag)
#Date and Time
All ISO 8601 strings:
1t.datetime() // "2024-01-15T14:30:00.000Z"2t.date() // "2024-01-15"3t.time() // "14:30:00"4t.duration() // "P1DT2H30M"
#Files
1t.file() // any file2t.file({ maxSize: 5 }) // max 5 MB
Used in $action for uploads:
1upload = $action({2 path: "/avatar",3 schema: { body: t.object({ file: t.file({ maxSize: 5 }) }) },4 handler: async ({ body }) => {5 // body.file is a FileLike object6 },7});
#Enums and Literals
1t.const("active") // exactly "active"2t.const(42) // exactly 423t.enum(["pending", "active", "done"]) // one of these
#Unions
1t.union([t.text(), t.null()]) // string | null2t.union([t.const("a"), t.const("b")]) // "a" | "b"
Avoid unions when possible. They're harder to work with in the ORM, validation errors are confusing, and OpenAPI doesn't love them. Prefer t.enum() for string choices or t.nullable() for optional values.
#Optional vs Nullable
1// optional: can be omitted 2t.object({ 3 name: t.text(), 4 nickname: t.optional(t.text()), 5}) 6// { name: string; nickname?: string } 7 8// nullable: must be present, can be null 9t.object({10 name: t.text(),11 deletedAt: t.nullable(t.datetime()),12})13// { name: string; deletedAt: string | null }
t.nullify(schema) makes all properties nullable at once.
#Schema Manipulation
1// pick specific fields 2t.pick(userSchema, ["id", "name"]) 3 4// remove fields 5t.omit(userSchema, ["password"]) 6 7// make everything optional 8t.partial(userSchema) 9 10// extend with new fields11t.extend(baseSchema, { newField: t.text() })
#Compound Types
#Objects
1t.object({2 name: t.text(),3 age: t.integer(),4})
Additional properties are rejected by default. We're strict here.
#Arrays
1t.array(t.text()) // max 1000 items by default2t.array(t.text(), { maxItems: 50 }) // custom limit3t.array(t.text(), { minItems: 1 }) // non-empty
The default limit prevents someone from sending you a million items.
#Records and Tuples
1t.record(t.text(), t.number()) // { [key: string]: number }2t.tuple([t.text(), t.number()]) // [string, number]
#Utility Types
1t.any() // escape hatch2t.void() // nothing3t.null() // null4t.undefined() // undefined5t.json() // { [key: string]: any }
#Customizing Defaults
Alepha sets sane defaults. Change them if you need:
1import { TypeProvider } from "alepha";2 3TypeProvider.DEFAULT_STRING_MAX_LENGTH = 255; // t.text()4TypeProvider.DEFAULT_SHORT_STRING_MAX_LENGTH = 64; // t.shortText()5TypeProvider.DEFAULT_LONG_STRING_MAX_LENGTH = 1024; // t.longText()6TypeProvider.DEFAULT_RICH_STRING_MAX_LENGTH = 65535; // t.richText()7TypeProvider.DEFAULT_ARRAY_MAX_ITEMS = 1000; // t.array()
Or override per-field:
1t.text({ maxLength: 500 })2t.array(t.text(), { maxItems: 10 })
#Raw TypeBox
Need something we don't wrap? Access TypeBox directly:
1import { Type } from "alepha";2 3const schema = Type.String({ format: "custom" });
#Quick Reference
| Need | Use |
|---|---|
| User input | t.text() |
| Internal string | t.string() |
| Short label | t.shortText() |
| Long description | t.longText() |
| Rich content | t.richText() |
t.email() |
|
| UUID | t.uuid() |
| Phone | t.e164() |
| Timestamp | t.datetime() |
| Date only | t.date() |
| File upload | t.file() |
| Exact value | t.const("x") |
| Choices | t.enum(["a", "b"]) |
| Optional | t.optional(schema) |
| Nullable | t.nullable(schema) |
| Pick fields | t.pick(schema, ["a"]) |
| Remove fields | t.omit(schema, ["b"]) |
| All optional | t.partial(schema) |
| Add fields | t.extend(schema, {}) |
| Get TS type | Static<typeof schema> |
Define your schema once. Alepha handles the rest.