Skip to main content
Learn about the type structure of services.generated.ts generated by Sonamu and how to use it.

Shared Types Overview

Single File

services.generated.tsAll types in one place

Auto Generated

Extracted from backendNo manual work needed

Type Reuse

Exported typesUse throughout project

Consistency

Single source of truthNo type conflicts

services.generated.ts Structure

File Overview

Sonamu generates all types and Services in a single file.
// services.generated.ts (auto-generated)

// 1. Common Imports
import { useQuery, useMutation, queryOptions } from "@tanstack/react-query";
import qs from "qs";

// 2. Entity Types
export interface User {
  id: number;
  username: string;
  email: string;
  createdAt: Date;
}

// 3. Subset Types
export type UserSubsetKey = "A" | "B" | "C";
export type UserSubsetMapping = {
  A: { id: number; username: string };
  B: { id: number; username: string; email: string };
  C: User;
};

// 4. Service Namespace
export namespace UserService {
  // Service functions
  export async function getUser<T extends UserSubsetKey>(
    subset: T,
    id: number
  ): Promise<UserSubsetMapping[T]> {
    return fetch({
      method: "GET",
      url: `/api/user/findById?${qs.stringify({ subset, id })}`,
    });
  }
  
  // TanStack Query Hook
  export const getUserQueryOptions = <T extends UserSubsetKey>(
    subset: T,
    id: number
  ) =>
    queryOptions({
      queryKey: ["User", "getUser", subset, id],
      queryFn: () => getUser(subset, id),
    });
  
  export const useUser = <T extends UserSubsetKey>(
    subset: T,
    id: number,
    options?: { enabled?: boolean }
  ) =>
    useQuery({
      ...getUserQueryOptions(subset, id),
      ...options,
    });
}

// 5. Other Entities follow same structure
export interface Post { /* ... */ }
export namespace PostService { /* ... */ }
File size:
  • Usually 1,000 ~ 5,000 lines
  • Can be 10,000+ lines with many Entities and APIs
  • But all auto-generated so no management burden

Type Categories

1. Entity Types

Represents database table structure.
// User Entity
export interface User {
  id: number;
  username: string;
  email: string;
  role: "admin" | "user";
  bio: string | null;
  createdAt: Date;
  updatedAt: Date;
}

// Post Entity
export interface Post {
  id: number;
  title: string;
  content: string;
  author_id: number;
  published: boolean;
  createdAt: Date;
}
Characteristics:
  • Exactly same as backend Entity
  • All field types accurately mapped
  • null, undefined, union types all preserved

2. Subset Types

Defines subsets of Entity.
// Subset Key (literal union)
export type UserSubsetKey = "A" | "B" | "C";

// Subset Mapping (type per Key)
export type UserSubsetMapping = {
  A: Pick<User, "id" | "username">;
  B: Pick<User, "id" | "username" | "email">;
  C: User; // All fields
};

// Use as Mapped Type
type SubsetA = UserSubsetMapping["A"];
// { id: number; username: string }
Subset naming convention:
  • A: Minimum fields (id + 1~2 core fields)
  • B: Medium fields (A + additional info)
  • C: All fields

3. API Parameter Types

Defines API function parameters.
export namespace UserService {
  // Explicit parameter types
  export async function updateProfile(params: {
    username?: string;
    bio?: string;
    avatar?: string;
  }): Promise<{ user: User }> {
    return fetch({
      method: "PUT",
      url: "/api/user/updateProfile",
      data: params,
    });
  }
  
  // Complex search parameters
  export async function search(query: {
    keyword: string;
    role?: "admin" | "user";
    isActive?: boolean;
    page?: number;
    pageSize?: number;
  }): Promise<{ users: User[]; total: number }> {
    return fetch({
      method: "GET",
      url: `/api/user/search?${qs.stringify(query)}`,
    });
  }
}

4. API Response Types

Defines API function return types.
export namespace UserService {
  // Simple response
  export async function getUser(
    id: number
  ): Promise<{ user: User }> {
    // ...
  }
  
  // Complex response
  export async function getDashboard(): Promise<{
    user: User;
    stats: {
      postCount: number;
      followerCount: number;
      viewCount: number;
    };
    recentPosts: Post[];
    recentComments: Comment[];
  }> {
    // ...
  }
}
Types used in React Hooks.
export namespace UserService {
  // Query Options type (reusable)
  export const getUserQueryOptions = (id: number) =>
    queryOptions({
      queryKey: ["User", "getUser", id],
      queryFn: () => getUser(id),
    });
  
  // Hook type (auto-inferred)
  export const useUser = (
    id: number,
    options?: { enabled?: boolean }
  ) =>
    useQuery({
      ...getUserQueryOptions(id),
      ...options,
    });
}

// Types auto-inferred on usage
const { data, isLoading } = UserService.useUser(123);
// data type: { user: User } | undefined

Type Reuse

Using Type Helpers

Reuse existing types with TypeScript’s utility types.
import type { UserService, User } from "@/services/services.generated";

// 1. Extract function return type
type UserProfile = Awaited<ReturnType<typeof UserService.getProfile>>;
// { user: User; stats: { postCount: number; ... } }

// 2. Extract parameter type
type UpdateParams = Parameters<typeof UserService.updateProfile>[0];
// { username?: string; bio?: string; avatar?: string }

// 3. Entity partial type
type UserBasic = Pick<User, "id" | "username" | "email">;

// 4. All fields optional
type PartialUser = Partial<User>;

// 5. Exclude specific fields
type UserWithoutDates = Omit<User, "createdAt" | "updatedAt">;

Component Props

Use generated types as Props.
import type { User, Post } from "@/services/services.generated";

// Use Entity type directly
interface UserCardProps {
  user: User;
}

function UserCard({ user }: UserCardProps) {
  return (
    <div>
      <h3>{user.username}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// Combine multiple Entities
interface DashboardProps {
  user: User;
  posts: Post[];
}

function Dashboard({ user, posts }: DashboardProps) {
  return (
    <div>
      <h1>Welcome, {user.username}</h1>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

Form Data Types

Use API parameter types as form data.
import { useState } from "react";
import type { UserService } from "@/services/services.generated";

// Extract parameter type
type UpdateProfileParams = Parameters<typeof UserService.updateProfile>[0];

function EditProfileForm() {
  // Type-safe state
  const [formData, setFormData] = useState<UpdateProfileParams>({
    username: "",
    bio: "",
    avatar: "",
  });
  
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    await UserService.updateProfile(formData); // βœ… Types match
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.username}
        onChange={(e) => setFormData({ ...formData, username: e.target.value })}
      />
      {/* ... */}
    </form>
  );
}

State Management

Reuse types in global state as well.
import { create } from "zustand";
import type { User } from "@/services/services.generated";

// Zustand Store
interface AuthStore {
  user: User | null;
  login: (user: User) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

Namespace Usage

Service Grouping

Services are grouped by Entity using Namespace.
// services.generated.ts
export namespace UserService {
  export async function getUser() { /* ... */ }
  export async function updateUser() { /* ... */ }
  export async function deleteUser() { /* ... */ }
  export const useUser = () => { /* ... */ };
}

export namespace PostService {
  export async function getPost() { /* ... */ }
  export async function createPost() { /* ... */ }
  export const usePost = () => { /* ... */ };
}
Usage:
// βœ… Clearly distinguished by Namespace
import { UserService, PostService } from "@/services/services.generated";

await UserService.getUser("A", 123);
await PostService.getPost("A", 456);
Benefits:
  • Prevents name collisions (getUser vs getPost)
  • Related functions logically grouped
  • Imports become concise
  • IDE auto-completion becomes more accurate

Type Import

Use type keyword when importing types only.
// βœ… Import types only (reduces bundle size)
import type { User, Post } from "@/services/services.generated";

// ❌ Full import (includes unnecessary code)
import { User, Post } from "@/services/services.generated";
Tree-shaking:
  • import type is removed after compilation
  • Bundle size optimization
  • Build speed improvement

File Size Management

Large-scale Projects

Files can become very large with many Entities and APIs.
// services.generated.ts (example)
// 100 Entities Γ— average 50 lines = 5,000 lines
// + Service functions = 10,000+ lines
But it’s okay:
  1. βœ… Auto-generated: No manual management needed
  2. βœ… Tree-shaking: Unused code excluded from bundle
  3. βœ… IDE performance: Modern IDEs handle large files fine
  4. βœ… Type checking: TypeScript compiler processes efficiently

Code Splitting

You can split code with dynamic import if needed.
// βœ… Load only when needed
async function loadUserService() {
  const { UserService } = await import("@/services/services.generated");
  return UserService;
}

// Usage
const UserService = await loadUserService();
await UserService.getUser("A", 123);

Version Control

Include in Git or Not

Include (recommended):
# Include services.generated.ts in Git
# (comment out or remove)
# services.generated.ts
Benefits:
  • Ready to use immediately after pull
  • Can develop frontend without backend
  • Can track type change history
Don’t include:
# Exclude services.generated.ts from Git
services.generated.ts
Benefits:
  • Less conflicts
  • Each person generates latest version
  • Git history stays clean
Recommendation: Including is generally more convenient.

Resolving Conflicts

When merge conflict occurs:
# 1. Pull latest code
git pull origin main

# 2. Regenerate Service
pnpm generate

# 3. Resolve conflict with regenerated file
git add services.generated.ts
git commit -m "resolve: regenerate services"

Debugging

Check Generated Types

You can check types directly in IDE.
import { UserService } from "@/services/services.generated";

// Hover over function
UserService.getUser
// ↓ IDE shows type
// (alias) getUser<T extends UserSubsetKey>(
//   subset: T,
//   id: number
// ): Promise<UserSubsetMapping[T]>
Shortcut:
  • VSCode: Cmd + Click (Mac) / Ctrl + Click (Windows)
  • Jump directly to type definition

Debugging Type Errors

When type error occurs:
  1. Check error message
Property 'username' does not exist on type 'User'.
  1. Check type definition
// Check User type in services.generated.ts
export interface User {
  id: number;
  displayName: string; // Not username!
  email: string;
}
  1. Check backend
// Check actual definition in backend
@api({ httpMethod: "GET" })
async getUser(): Promise<{ user: { displayName: string } }> {
  // displayName, not username!
}
  1. Fix code
// username β†’ displayName
console.log(user.displayName);

Cautions

Cautions when using shared types:
  1. Don’t manually modify services.generated.ts
  2. Use import type for bundle size optimization
  3. pnpm generate required on API changes
  4. Access Services through Namespace
  5. Use Type Helpers when reusing types

Next Steps