메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
Sonamuμ—μ„œ 자주 λ°œμƒν•˜λŠ” TypeScript νƒ€μž… κ΄€λ ¨ 였λ₯˜μ™€ ν•΄κ²° 방법을 λ‹€λ£Ήλ‹ˆλ‹€.

Reserved Keywords 좩돌

증상

class UserModelClass extends BaseModel {
  async delete(id: number) {  // ❌
    // deleteλŠ” JavaScript μ˜ˆμ•½μ–΄
  }
}
λŸ°νƒ€μž„ μ—λŸ¬:
Unexpected token 'delete'
λ˜λŠ” Sonamu sync μ‹œ:
Error: Reserved keyword 'delete' cannot be used as method name

원인

JavaScript/TypeScript의 μ˜ˆμ•½μ–΄λ₯Ό λ©”μ„œλ“œλͺ…μ΄λ‚˜ 속성λͺ…μœΌλ‘œ μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

μ˜ˆμ•½μ–΄ μ‚¬μš©μ„ ν”Όν•˜κ±°λ‚˜ λ‹€λ₯Έ 이름 μ‚¬μš©:
// ❌ μ˜ˆμ•½μ–΄ μ‚¬μš©
async delete() { }
async switch() { }
async return() { }

// βœ… λ‹€λ₯Έ 이름 μ‚¬μš©
async remove() { }
async del() { }
async toggle() { }
async getReturn() { }
Sonamuκ°€ κ²€μ¦ν•˜λŠ” 72개 μ˜ˆμ•½μ–΄:
const RESERVED_KEYWORDS = [
  "break", "case", "catch", "class", "const", "continue", "debugger",
  "default", "delete", "do", "else", "enum", "export", "extends",
  "false", "finally", "for", "function", "if", "import", "in",
  "instanceof", "new", "null", "return", "super", "switch",
  "this", "throw", "true", "try", "typeof", "var", "void",
  "while", "with", "yield", "let", "static", "implements",
  "interface", "package", "private", "protected", "public",
  "await", "abstract", "as", "asserts", "any", "async",
  "boolean", "constructor", "declare", "get", "infer", "is",
  "keyof", "module", "namespace", "never", "readonly", "require",
  "number", "object", "set", "string", "symbol", "type",
  "undefined", "unique", "unknown", "from", "of"
];

νƒ€μž… μΆ”λ‘  μ‹€νŒ¨

증상

const users = await UserModel.findMany();
// Type: any[]  ❌ νƒ€μž…μ΄ μ œλŒ€λ‘œ μΆ”λ‘ λ˜μ§€ μ•ŠμŒ

원인

  1. Sonamu syncerκ°€ μ œλŒ€λ‘œ μ‹€ν–‰λ˜μ§€ μ•ŠμŒ
  2. .generated 파일이 였래됨
  3. TypeScript μ„œλ²„ μΊμ‹œ 문제

ν•΄κ²° 방법

1. Syncer μž¬μ‹€ν–‰

pnpm sonamu sync

2. TypeScript μ„œλ²„ μž¬μ‹œμž‘

VSCode:
Command Palette (Cmd+Shift+P)
> TypeScript: Restart TS Server

3. Generated 파일 확인

// src/application/sonamu.generated.ts
export type UserSave = {
  id?: number;
  email: string;
  name: string;
  // ...
};
파일이 μ—†κ±°λ‚˜ μ˜€λž˜λ˜μ—ˆλ‹€λ©΄ sync μž¬μ‹€ν–‰.

BaseModel λ©”μ„œλ“œ νƒ€μž… 였λ₯˜

증상

class UserModelClass extends BaseModel {
  async findByEmail(email: string) {
    return this.findOne({ email });
    // Error: Property 'findOne' does not exist on type 'UserModelClass'
  }
}

원인

BaseModel의 auto-generated λ©”μ„œλ“œ νƒ€μž…μ΄ μ œλŒ€λ‘œ μ μš©λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

1. entity.json 확인

{
  "properties": {
    "id": { "type": "id" },
    "email": { "type": "string" },
    "name": { "type": "string" }
  }
}

2. Model 클래슀 μ •μ˜ 확인

class UserModelClass extends BaseModel<User, UserSave> {
  entityName = "User" as const;
  // ...
}

export const UserModel = new UserModelClass();

3. Syncer둜 νƒ€μž… μž¬μƒμ„±

pnpm sonamu sync

Union νƒ€μž… 였λ₯˜

증상

type OrderStatus = "pending" | "processing" | "completed";

class OrderModelClass extends BaseModel {
  async updateStatus(id: number, status: string) {  // ❌
    // statusλŠ” OrderStatusμ—¬μ•Ό 함
  }
}
νƒ€μž… μ•ˆμ „μ„±μ΄ 보μž₯λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

λͺ…ν™•ν•œ νƒ€μž… μ§€μ •:
type OrderStatus = "pending" | "processing" | "completed";

class OrderModelClass extends BaseModel {
  async updateStatus(id: number, status: OrderStatus) {  // βœ…
    return this.updateOne(
      { id },
      { status }
    );
  }
}

Zod μŠ€ν‚€λ§ˆ νƒ€μž… 뢈일치

증상

const UserSchema = z.object({
  email: z.string(),
  age: z.number()
});

@api()
async createUser(email: string, age: string) {  // ❌ ageλŠ” numberμ—¬μ•Ό 함
  // ...
}

원인

API λ©”μ„œλ“œ νŒŒλΌλ―Έν„° νƒ€μž…κ³Ό Zod μŠ€ν‚€λ§ˆκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

1. νŒŒλΌλ―Έν„° νƒ€μž… μˆ˜μ •

@api()
async createUser(email: string, age: number) {  // βœ…
  // Sonamuκ°€ μžλ™μœΌλ‘œ Zod 검증
}

2. λͺ…μ‹œμ  Zod μŠ€ν‚€λ§ˆ μ‚¬μš©

const CreateUserSchema = z.object({
  email: z.string().email(),
  age: z.number().int().positive()
});

@api({ schema: CreateUserSchema })
async createUser(data: z.infer<typeof CreateUserSchema>) {
  // νƒ€μž… μ•ˆμ „μ„± 보μž₯
}

Intersection νƒ€μž… 였λ₯˜

증상

type WithTimestamps = {
  created_at: Date;
  updated_at: Date;
};

type User = {
  id: number;
  email: string;
} & WithTimestamps;  // Intersection νƒ€μž…

// νƒ€μž…μ΄ λ³΅μž‘ν•˜κ²Œ ν‘œμ‹œλ¨

ν•΄κ²° 방법

Sonamu 0.7.29+λΆ€ν„°λŠ” intersection/union νƒ€μž…μ„ μžλ™μœΌλ‘œ κ΄„ν˜Έλ‘œ κ°μŒ‰λ‹ˆλ‹€:
// μƒμ„±λœ νƒ€μž…
type User = {
  id: number;
  email: string;
} & (WithTimestamps);  // κ΄„ν˜Έ μΆ”κ°€λ‘œ λͺ…ν™•μ„± ν–₯상
μ—…λ°μ΄νŠΈ ν›„ sync μž¬μ‹€ν–‰:
pnpm add sonamu@latest
pnpm sonamu sync

Template Literal νƒ€μž… 였λ₯˜

증상

type EventName = `user:${string}`;

// Zod v4μ—μ„œ template literal νƒ€μž… μ‚¬μš© μ‹œ 였λ₯˜

원인

Zod v4μ—μ„œ template literal νƒ€μž… 처리 방식이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

SonamuλŠ” μžλ™μœΌλ‘œ backslash escaping 처리:
// entity.json
{
  "columns": {
    "event_name": {
      "type": "string",
      "literalType": "user:${string}"  // μžλ™μœΌλ‘œ 처리됨
    }
  }
}
μƒμ„±λœ Zod μŠ€ν‚€λ§ˆ:
z.literal(`user:$\{string}`)  // μ˜¬λ°”λ₯΄κ²Œ escape됨

μˆœν™˜ μ°Έμ‘° νƒ€μž… 였λ₯˜

증상

// user.model.ts
export class UserModelClass extends BaseModel {
  // ...
}
export type User = { ... posts: Post[] };

// post.model.ts
export class PostModelClass extends BaseModel {
  // ...
}
export type Post = { ... author: User };

// Error: Circular dependency detected

원인

두 entityκ°€ μ„œλ‘œλ₯Ό μ°Έμ‘°ν•˜μ—¬ μˆœν™˜ μ˜μ‘΄μ„±μ΄ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

1. Type-only import μ‚¬μš©

// user.model.ts
import type { Post } from "../post/post.model";

export type User = {
  id: number;
  email: string;
  posts: Post[];
};
// post.model.ts
import type { User } from "../user/user.model";

export type Post = {
  id: number;
  title: string;
  author: User;
};

2. 곡톡 νƒ€μž… 파일 생성

// types/index.ts
export type { User } from "../application/user/user.model";
export type { Post } from "../application/post/post.model";
// λ‹€λ₯Έ νŒŒμΌμ—μ„œ μ‚¬μš©
import type { User, Post } from "@/types";

파일 νƒ€μž… 였λ₯˜ (@upload)

증상

@api()
@upload({ mode: "single" })
async uploadFile() {
  const { file } = Sonamu.getUploadContext();
  // Type: UploadedFile | undefined
  
  const buffer = file.buffer;  // ❌ Property 'buffer' does not exist
}

원인

UploadedFile ν΄λž˜μŠ€μ— buffer 속성이 μ—†μŠ΅λ‹ˆλ‹€. toBuffer() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

ν•΄κ²° 방법

@api()
@upload({ mode: "single" })
async uploadFile() {
  const { file } = Sonamu.getUploadContext();
  
  if (!file) {
    throw new BadRequestException("파일이 ν•„μš”ν•©λ‹ˆλ‹€");
  }
  
  // βœ… toBuffer() λ©”μ„œλ“œ μ‚¬μš©
  const buffer = await file.toBuffer();
  
  // λ˜λŠ” 파일 μ €μž₯
  const url = await file.saveToDisk(`uploads/${file.filename}`);
  
  return { url, size: file.size };
}

νƒ€μž… κ°€λ“œ 였λ₯˜

증상

function isUser(value: any): boolean {  // ❌
  return value && typeof value.id === "number";
}

if (isUser(data)) {
  console.log(data.email);  // Error: Property 'email' does not exist
}

원인

νƒ€μž… κ°€λ“œ ν•¨μˆ˜κ°€ νƒ€μž… 쒁히기(type narrowing)λ₯Ό ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

Type predicate μ‚¬μš©:
function isUser(value: any): value is User {  // βœ…
  return (
    value &&
    typeof value.id === "number" &&
    typeof value.email === "string"
  );
}

if (isUser(data)) {
  console.log(data.email);  // βœ… νƒ€μž… μ•ˆμ „
}

μ œλ„€λ¦­ νƒ€μž… μΆ”λ‘  μ‹€νŒ¨

증상

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json();  // Type: any
}

const users = await fetchData("/api/users");
// Type: unknown  ❌

ν•΄κ²° 방법

λͺ…μ‹œμ  νƒ€μž… μ§€μ •:
const users = await fetchData<User[]>("/api/users");
// Type: User[]  βœ…
λ˜λŠ” νƒ€μž… 검증:
async function fetchData<T>(
  url: string,
  validator: (data: unknown) => data is T
): Promise<T> {
  const response = await fetch(url);
  const data = await response.json();
  
  if (!validator(data)) {
    throw new Error("Invalid data format");
  }
  
  return data;
}

// μ‚¬μš©
const users = await fetchData("/api/users", isUserArray);

κ΄€λ ¨ λ¬Έμ„œ