Learn about the type structure of services.generated.ts generated by Sonamu and how to use it.
Shared Types Overview
Single File services.generated.ts All types in one place
Auto Generated Extracted from backend No manual work needed
Type Reuse Exported types Use throughout project
Consistency Single source of truth No 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 >
);
}
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 :
β
Auto-generated : No manual management needed
β
Tree-shaking : Unused code excluded from bundle
β
IDE performance : Modern IDEs handle large files fine
β
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:
Check error message
Property 'username' does not exist on type 'User'.
Check type definition
// Check User type in services.generated.ts
export interface User {
id : number ;
displayName : string ; // Not username!
email : string ;
}
Check backend
// Check actual definition in backend
@ api ({ httpMethod: "GET" })
async getUser (): Promise < { user : { displayName : string } } > {
// displayName, not username!
}
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
API Type Inference Understanding type inference
Compile-time Errors Error detection methods
Using Services Service usage
Custom Components Writing components