Skip to content

InferType ​

InferType is a TypeScript utility type that extracts the runtime type of entities from Routier schemas. It provides compile-time type safety by inferring the actual TypeScript type that corresponds to your schema definition.

Quick Navigation ​

What is InferType? ​

InferType takes a compiled schema and returns the TypeScript type that represents the actual entity structure at runtime. This includes:

  • All properties defined in your schema
  • Applied modifiers like optional(), nullable(), default()
  • Nested objects and arrays with their complete structure
  • Computed properties and their return types

Basic Usage ​

ts
import { InferType, InferCreateType, s } from "@routier/core/schema";

// Define a schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    email: s.string().distinct(),
    name: s.string(),
    age: s.number().optional(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Extract the TypeScript type
type User = InferType<typeof userSchema>;
type CreateUser = InferCreateType<typeof userSchema>;

// User type includes all properties (including defaults and identities)
const user: User = {
    id: "user-123",           // Required (identity)
    email: "[email protected]", // Required
    name: "John Doe",         // Required
    age: 30,                  // Optional
    createdAt: new Date(),    // Required (has default)
};

// CreateUser type excludes defaults and identities
const newUser: CreateUser = {
    email: "[email protected]", // Required
    name: "Jane Doe",          // Required
    age: 25,                   // Optional
    // id and createdAt are optional (auto-generated/defaulted)
};

InferType vs InferCreateType ​

Routier provides two related type utilities:

InferType ​

  • Purpose: Represents the complete entity after creation
  • Includes: All properties, including those with defaults and identities
  • Use case: Working with existing entities from the database

InferCreateType ​

  • Purpose: Represents the entity structure for creation
  • Excludes: Properties with defaults (optional) and identity properties (auto-generated)
  • Use case: Creating new entities with addAsync()
ts
import { InferType, InferCreateType, s } from "@routier/core/schema";

const productSchema = s.define("products", {
    id: s.string().key().identity(),
    name: s.string(),
    price: s.number(),
    category: s.string().default("general"),
    isActive: s.boolean().default(true),
}).compile();

type Product = InferType<typeof productSchema>;
type CreateProduct = InferCreateType<typeof productSchema>;

// InferType - Complete entity type
const existingProduct: Product = {
    id: "prod-123",        // Required (identity)
    name: "Widget",        // Required
    price: 29.99,         // Required
    category: "general",  // Required (has default)
    isActive: true,       // Required (has default)
};

// InferCreateType - Creation type (defaults are optional)
const newProduct: CreateProduct = {
    name: "Gadget",       // Required
    price: 19.99,         // Required
    // category and isActive are optional (have defaults)
    // id is optional (auto-generated)
};

// TypeScript will enforce these differences
function updateProduct(product: Product, updates: Partial<Product>) {
    // Can update any property of existing product
    return { ...product, ...updates };
}

function createProduct(data: CreateProduct) {
    // Only requires non-default, non-identity properties
    return data;
}

Real-World Examples ​

Function Parameters ​

ts
import { InferType, s } from "@routier/core/schema";

const userSchema = s.define("users", {
    id: s.string().key().identity(),
    email: s.string().distinct(),
    name: s.string(),
    profile: s.object({
        bio: s.string().optional(),
        avatar: s.string().optional(),
        preferences: s.object({
            theme: s.string("light", "dark").default("light"),
            notifications: s.boolean().default(true),
        }),
    }),
}).compile();

type User = InferType<typeof userSchema>;

// Type-safe function parameters
function sendEmail(user: User, subject: string, body: string) {
    // TypeScript knows user.email exists and is a string
    console.log(`Sending email to ${user.email}`);
    console.log(`Subject: ${subject}`);
    console.log(`Body: ${body}`);
}

function updateUserProfile(user: User, updates: Partial<User['profile']>) {
    // TypeScript knows the exact structure of user.profile
    return {
        ...user,
        profile: { ...user.profile, ...updates }
    };
}

function getUserPreferences(user: User) {
    // TypeScript knows user.profile.preferences structure
    return {
        theme: user.profile.preferences.theme,
        notifications: user.profile.preferences.notifications,
    };
}

// Usage with full type safety
const user: User = {
    id: "user-123",
    email: "[email protected]",
    name: "John Doe",
    profile: {
        bio: "Software developer",
        preferences: {
            theme: "dark",
            notifications: true,
        },
    },
};

sendEmail(user, "Welcome!", "Thanks for joining!");
updateUserProfile(user, { bio: "Senior developer" });
const prefs = getUserPreferences(user);

API Responses ​

ts
import { InferType, s } from "@routier/core/schema";

const orderSchema = s.define("orders", {
    id: s.string().key().identity(),
    customerId: s.string(),
    items: s.array(s.object({
        productId: s.string(),
        quantity: s.number(),
        price: s.number(),
    })),
    status: s.string("pending", "processing", "shipped", "delivered"),
    total: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

type Order = InferType<typeof orderSchema>;

// API response type safety
interface ApiResponse<T> {
    data: T;
    success: boolean;
    message: string;
}

type OrderResponse = ApiResponse<Order>;
type OrdersResponse = ApiResponse<Order[]>;

// Type-safe API functions
async function getOrder(id: string): Promise<OrderResponse> {
    // Implementation would fetch from API
    return {
        data: {
            id,
            customerId: "customer-123",
            items: [
                { productId: "prod-1", quantity: 2, price: 29.99 }
            ],
            status: "pending",
            total: 59.98,
            createdAt: new Date(),
        },
        success: true,
        message: "Order retrieved successfully",
    };
}

async function getOrders(): Promise<OrdersResponse> {
    // Implementation would fetch from API
    return {
        data: [],
        success: true,
        message: "Orders retrieved successfully",
    };
}

// Type-safe response handling
async function processOrderResponse() {
    const response = await getOrder("order-123");

    if (response.success) {
        const order = response.data; // TypeScript knows this is Order type

        // Full type safety on order properties
        console.log(`Order ${order.id} status: ${order.status}`);
        console.log(`Total: $${order.total}`);
        console.log(`Items: ${order.items.length}`);

        // TypeScript knows items structure
        order.items.forEach(item => {
            console.log(`${item.productId}: ${item.quantity} x $${item.price}`);
        });
    }
}

Complex Nested Types ​

ts
import { InferType, s } from "@routier/core/schema";

const companySchema = s.define("companies", {
    id: s.string().key().identity(),
    name: s.string(),
    address: s.object({
        street: s.string(),
        city: s.string(),
        state: s.string(),
        zipCode: s.string(),
        country: s.string().default("USA"),
    }),
    employees: s.array(s.object({
        id: s.string(),
        name: s.string(),
        email: s.string(),
        department: s.string(),
        role: s.string("admin", "manager", "employee"),
        salary: s.number().optional(),
        startDate: s.date(),
    })),
    departments: s.array(s.object({
        id: s.string(),
        name: s.string(),
        managerId: s.string().optional(),
        budget: s.number().optional(),
    })),
    metadata: s.object({
        founded: s.date(),
        industry: s.string(),
        size: s.string("startup", "small", "medium", "large"),
        isPublic: s.boolean().default(false),
    }),
}).compile();

type Company = InferType<typeof companySchema>;

// Complex type operations with full type safety
function findEmployeeByEmail(company: Company, email: string) {
    return company.employees.find(emp => emp.email === email);
}

function getDepartmentEmployees(company: Company, departmentId: string) {
    return company.employees.filter(emp => emp.department === departmentId);
}

function calculateTotalSalary(company: Company) {
    return company.employees
        .filter(emp => emp.salary !== undefined)
        .reduce((total, emp) => total + emp.salary!, 0);
}

function getCompanySummary(company: Company) {
    return {
        name: company.name,
        totalEmployees: company.employees.length,
        departments: company.departments.length,
        totalSalary: calculateTotalSalary(company),
        founded: company.metadata.founded,
        industry: company.metadata.industry,
        size: company.metadata.size,
    };
}

// Type-safe nested property access
function updateEmployeeRole(company: Company, employeeId: string, newRole: Company['employees'][0]['role']) {
    const employee = company.employees.find(emp => emp.id === employeeId);
    if (employee) {
        employee.role = newRole; // TypeScript ensures newRole is valid
    }
}

// Usage example
const company: Company = {
    id: "comp-123",
    name: "TechCorp",
    address: {
        street: "123 Tech St",
        city: "San Francisco",
        state: "CA",
        zipCode: "94105",
        country: "USA",
    },
    employees: [
        {
            id: "emp-1",
            name: "John Doe",
            email: "[email protected]",
            department: "engineering",
            role: "manager",
            salary: 120000,
            startDate: new Date("2020-01-15"),
        },
    ],
    departments: [
        {
            id: "dept-1",
            name: "Engineering",
            managerId: "emp-1",
            budget: 500000,
        },
    ],
    metadata: {
        founded: new Date("2015-03-01"),
        industry: "Technology",
        size: "medium",
        isPublic: false,
    },
};

// All operations are fully type-safe
const john = findEmployeeByEmail(company, "[email protected]");
const engineeringTeam = getDepartmentEmployees(company, "dept-1");
const summary = getCompanySummary(company);
updateEmployeeRole(company, "emp-1", "admin");

Type Safety Benefits ​

Compile-Time Type Checking ​

ts
// ✅ TypeScript will catch this error at compile time
function processUser(user: User) {
  console.log(user.email); // ✅ Valid - email exists on User type
  console.log(user.invalidField); // ❌ Error - property doesn't exist
}

IntelliSense Support ​

ts
const user: User = await ctx.users.firstAsync();
user. // ← IntelliSense shows all available properties

Refactoring Safety ​

When you change your schema, TypeScript will show errors everywhere the type is used, ensuring you update all related code.

Best Practices ​

1. Use Type Aliases ​

ts
// ✅ Good - reusable type alias
type User = InferType<typeof userSchema>;
type CreateUser = InferCreateType<typeof userSchema>;

// ❌ Avoid - repeating the type everywhere
function processUser(user: InferType<typeof userSchema>) { ... }

2. Export Types for Reuse ​

ts
// schema.ts
export const userSchema = s.define("users", { ... }).compile();
export type User = InferType<typeof userSchema>;
export type CreateUser = InferCreateType<typeof userSchema>;

// other-file.ts
import { User, CreateUser } from './schema';

3. Use Appropriate Type ​

ts
// ✅ Use InferCreateType for creation
async function createUser(data: CreateUser) {
  return await ctx.users.addAsync(data);
}

// ✅ Use InferType for existing entities
async function updateUser(user: User, updates: Partial<User>) {
  return await ctx.users.updateAsync(user, updates);
}

4. Use constrain() for Branded Types ​

When working with branded or tagged types (like UUIDs), use constrain() to narrow string types:

ts
type UUID = string & { __brand: "UUID" };

const userSchema = s
  .define("users", {
    id: s.string().constrain<UUID>().key().identity(),
    name: s.string(),
  })
  .compile();

type User = InferType<typeof userSchema>;
// User.id is now typed as UUID, not just string

Released under the MIT License.