Skip to content

TypeScript Types

Miguel Guevara edited this page Jan 9, 2026 · 1 revision

TypeScript Types

Learn how to use Dynamite's TypeScript types for type-safe development.


Overview

Dynamite provides specialized TypeScript types to ensure type safety throughout your application:

  • CreationOptional - Optional during create, required after
  • NonAttribute - Excluded from database (computed/relations)
  • InferAttributes - Extract DB attributes from model
  • InferRelations - Extract relations from model
  • CreateInput - Input type for create()
  • UpdateInput - Input type for update()

CreationOptional

Use CreationOptional<T> for fields that are optional during creation but exist after:

Basic Usage

class User extends Table<User> {
  // Optional during create (auto-generated)
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  // Required during create
  declare name: string;

  // Optional during create (has default)
  @Default(() => "customer")
  declare role: CreationOptional<string>;

  // Optional during create (auto-set)
  @CreatedAt()
  declare created_at: CreationOptional<string>;
}

// Type-safe creation
const user = await User.create({
  name: "John Doe"
  // id, role, created_at are optional
});

// After creation, all fields exist
console.log(user.id);         // string (not undefined)
console.log(user.name);       // string
console.log(user.role);       // string (not undefined)
console.log(user.created_at); // string (not undefined)

When to Use

Use CreationOptional<T> for fields with:

  • @Default() decorator
  • @CreatedAt() decorator
  • @UpdatedAt() decorator
  • @DeleteAt() decorator
  • Auto-generated values
class Post extends Table<Post> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>; // ✓

  declare title: string; // ✗ Required

  @Default(() => "draft")
  declare status: CreationOptional<string>; // ✓

  @Default(() => 0)
  declare views: CreationOptional<number>; // ✓

  @CreatedAt()
  declare created_at: CreationOptional<string>; // ✓

  @UpdatedAt()
  declare updated_at: CreationOptional<string>; // ✓
}

NonAttribute

Use NonAttribute<T> for properties that are not stored in the database:

Computed Properties

class User extends Table<User> {
  declare first_name: string;
  declare last_name: string;

  // Computed property - not stored
  declare full_name: NonAttribute<string>;

  constructor(data?: any) {
    super(data);
    Object.defineProperty(this, 'full_name', {
      get: () => `${this.first_name} ${this.last_name}`,
      enumerable: true
    });
  }
}

const user = await User.create({
  first_name: "John",
  last_name: "Doe"
});

console.log(user.full_name); // "John Doe" (computed, not in DB)

Relationships

class User extends Table<User> {
  @PrimaryKey()
  declare id: string;

  // Relations are NOT stored in DB
  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<Post[]>;

  @HasOne(() => Profile, "user_id")
  declare profile: NonAttribute<Profile | null>;

  @ManyToMany(() => Role, "user_roles", "user_id", "role_id")
  declare roles: NonAttribute<Role[]>;
}

Helper Methods

class User extends Table<User> {
  @PrimaryKey()
  declare id: string;

  declare email: string;

  // Method - not stored
  declare is_admin: NonAttribute<() => boolean>;

  constructor(data?: any) {
    super(data);
    this.is_admin = () => this.email.endsWith("@admin.com");
  }
}

Type Inference

InferAttributes

Extract database attributes from a model:

import { InferAttributes } from "@arcaelas/dynamite";

class User extends Table<User> {
  @PrimaryKey()
  declare id: string;

  declare name: string;
  declare email: string;

  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<Post[]>; // Excluded
}

// Type contains only: { id, name, email }
type UserAttributes = InferAttributes<User>;

function processUserData(data: UserAttributes) {
  console.log(data.id);    // ✓
  console.log(data.name);  // ✓
  console.log(data.email); // ✓
  console.log(data.posts); // ✗ Error: posts doesn't exist
}

InferRelations

Extract relations from a model:

import { InferRelations } from "@arcaelas/dynamite";

class User extends Table<User> {
  @PrimaryKey()
  declare id: string;

  declare name: string;

  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<Post[]>;

  @HasOne(() => Profile, "user_id")
  declare profile: NonAttribute<Profile | null>;
}

// Type contains only: { posts, profile }
type UserRelations = InferRelations<User>;

function processRelations(relations: UserRelations) {
  console.log(relations.posts);   // ✓ Post[]
  console.log(relations.profile); // ✓ Profile | null
  console.log(relations.id);      // ✗ Error: id doesn't exist
}

Input Types

CreateInput

Type-safe input for create() method:

import { CreateInput } from "@arcaelas/dynamite";

class User extends Table<User> {
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  declare name: string;
  declare email: string;

  @Default(() => "customer")
  declare role: CreationOptional<string>;

  @CreatedAt()
  declare created_at: CreationOptional<string>;
}

// Type: { name: string, email: string, id?: string, role?: string }
type UserCreateInput = CreateInput<User>;

async function createUser(data: UserCreateInput) {
  return await User.create(data);
}

// Valid
createUser({ name: "John", email: "john@example.com" });
createUser({ name: "Jane", email: "jane@example.com", role: "admin" });

// Invalid
createUser({ name: "John" }); // ✗ Error: email required
createUser({ email: "john@example.com" }); // ✗ Error: name required

UpdateInput

Type-safe input for update() method:

import { UpdateInput } from "@arcaelas/dynamite";

class User extends Table<User> {
  @PrimaryKey()
  declare id: string;

  declare name: string;
  declare email: string;
}

// Type: Partial<{ name: string, email: string }>
type UserUpdateInput = UpdateInput<User>;

async function updateUser(id: string, data: UserUpdateInput) {
  return await User.update(data, { id });
}

// Valid
updateUser("123", { name: "New Name" });
updateUser("123", { email: "new@example.com" });
updateUser("123", { name: "New Name", email: "new@example.com" });
updateUser("123", {}); // Also valid (no changes)

// Invalid
updateUser("123", { id: "456" }); // ✗ Error: can't update primary key

Advanced Type Patterns

Strict Model Definition

class User extends Table<User> {
  // Primary key (required after creation)
  @PrimaryKey()
  @Default(() => crypto.randomUUID())
  declare id: CreationOptional<string>;

  // Required fields
  declare name: string;
  declare email: string;

  // Optional fields with defaults
  @Default(() => "customer")
  declare role: CreationOptional<"customer" | "admin" | "moderator">;

  @Default(() => true)
  declare active: CreationOptional<boolean>;

  // Timestamps
  @CreatedAt()
  declare created_at: CreationOptional<string>;

  @UpdatedAt()
  declare updated_at: CreationOptional<string>;

  // Soft delete
  @DeleteAt()
  declare deleted_at: CreationOptional<string | null>;

  // Relations
  @HasMany(() => Post, "user_id")
  declare posts: NonAttribute<Post[]>;

  // Computed
  declare is_admin: NonAttribute<boolean>;

  constructor(data?: any) {
    super(data);
    Object.defineProperty(this, 'is_admin', {
      get: () => this.role === "admin",
      enumerable: false
    });
  }
}

Type Guards

function isUser(value: any): value is User {
  return value instanceof User;
}

function isPost(value: any): value is Post {
  return value instanceof Post;
}

async function processRecord(record: User | Post) {
  if (isUser(record)) {
    console.log(record.name); // ✓ Type is User
    console.log(record.posts); // ✓ Type is Post[]
  } else if (isPost(record)) {
    console.log(record.title); // ✓ Type is Post
    console.log(record.user); // ✓ Type is User | null
  }
}

Generic Functions

async function findById<T extends Table<T>>(
  Model: new () => T,
  id: string
): Promise<T | undefined> {
  return await Model.first({ id } as any);
}

// Type-safe usage
const user = await findById(User, "123"); // Type: User | undefined
const post = await findById(Post, "456"); // Type: Post | undefined

Best Practices

  1. Use CreationOptional for fields with:

    • @Default() decorator
    • @CreatedAt(), @UpdatedAt() decorators
    • Auto-generated values
  2. Use NonAttribute for:

    • Computed properties
    • Relationships
    • Helper methods
    • Transient data
  3. Leverage type inference:

    • Use InferAttributes for database operations
    • Use InferRelations for relation handling
    • Use CreateInput/UpdateInput for API boundaries
  4. Enable strict mode in tsconfig.json:

    {
      "compilerOptions": {
        "strict": true,
        "experimentalDecorators": true
      }
    }
  5. Avoid type assertions:

    // ✗ Bad
    const user = await User.first({ id: "123" }) as User;
    
    // ✓ Good
    const user = await User.first({ id: "123" });
    if (user) {
      // user is User type here
    }

Quick Reference

Type Purpose Usage
CreationOptional<T> Optional on create Fields with @Default, @CreatedAt, etc.
NonAttribute<T> Not stored in DB Computed properties, relations
InferAttributes<T> Extract DB attributes Database operations
InferRelations<T> Extract relations Relation handling
CreateInput<T> Input for create() API boundaries
UpdateInput<T> Input for update() API boundaries

See also:

Clone this wiki locally