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

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()
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

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

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

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

// ✅ 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

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

// ✅ 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

// 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

// ✅ 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);
}