alepha@docs:~/docs/concepts$
cat 5-type-safety.md
3 min read
Last commit:

#Type Safety (The t Object)

In many full-stack frameworks, you end up defining your data structures three times:

  1. Once for the Database (SQL/ORM)
  2. Once for the API Validation (Zod/Joi)
  3. 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:

typescript
1const userSchema = t.object({2  username: t.text(),3  age: t.integer()4});
  1. Runtime Validation: Used by $action to validate incoming HTTP JSON bodies.
  2. Database Definition: Used by $entity to generate CREATE TABLE statements.
  3. TypeScript Inference: Used by your IDE to give you autocomplete.
  4. 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:

typescript
 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.

typescript
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

typescript
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

typescript
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

typescript
1t.boolean()

Nothing fancy here.

#Format Types

typescript
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:

typescript
1t.datetime()   // "2024-01-15T14:30:00.000Z"2t.date()       // "2024-01-15"3t.time()       // "14:30:00"4t.duration()   // "P1DT2H30M"

#Files

typescript
1t.file()                 // any file2t.file({ maxSize: 5 })   // max 5 MB

Used in $action for uploads:

typescript
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

typescript
1t.const("active")                        // exactly "active"2t.const(42)                              // exactly 423t.enum(["pending", "active", "done"])    // one of these

#Unions

typescript
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

typescript
 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

typescript
 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

typescript
1t.object({2  name: t.text(),3  age: t.integer(),4})

Additional properties are rejected by default. We're strict here.

#Arrays

typescript
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

typescript
1t.record(t.text(), t.number())       // { [key: string]: number }2t.tuple([t.text(), t.number()])      // [string, number]

#Utility Types

typescript
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:

typescript
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:

typescript
1t.text({ maxLength: 500 })2t.array(t.text(), { maxItems: 10 })

#Raw TypeBox

Need something we don't wrap? Access TypeBox directly:

typescript
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()
Email 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.

On This Page
No headings found...
ready
mainTypeScript
UTF-8concepts_type_safety.md