t Object)In many full-stack frameworks, you end up defining your data structures three times:
If you change one, you have to change the others. It's exhausting and error-prone.
Alepha solves this with the t object.
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});
$action to validate incoming HTTP JSON bodies.$entity to generate CREATE TABLE statements.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 }
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
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.
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".
1t.boolean()
Nothing fancy here.
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)
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"
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});
1t.const("active") // exactly "active"2t.const(42) // exactly 423t.enum(["pending", "active", "done"]) // one of these
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.
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.
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() })
1t.object({2 name: t.text(),3 age: t.integer(),4})
Additional properties are rejected by default. We're strict here.
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.
1t.record(t.text(), t.number()) // { [key: string]: number }2t.tuple([t.text(), t.number()]) // [string, number]
1t.any() // escape hatch2t.void() // nothing3t.null() // null4t.undefined() // undefined5t.json() // { [key: string]: any }
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 })
Need something we don't wrap? Access TypeBox directly:
1import { Type } from "alepha";2 3const schema = Type.String({ format: "custom" });
| 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.