Ch 04. 객체 타입과 인터페이스
객체 리터럴 타입부터 interface의 확장, 선택적 프로퍼티, 읽기 전용 프로퍼티까지 다룹니다.
객체 리터럴 타입
// 파일: src/object-types.ts// 인라인으로 객체 타입 정의function printUser(user: { name: string; age: number }): void { console.log(`${user.name} (${user.age})`);}printUser({ name: "Alice", age: 30 });printUser({ name: "Bob" }); // Error: Property 'age' is missing
간단한 한 번짜리 타입은 인라인으로도 괜찮습니다. 재사용이 필요하면 interface나 type으로 빼세요.
interface 기본
// 파일: src/interface-basic.tsinterface User { id: number; name: string; email: string;}function getUser(id: number): User { return { id, name: "Alice", email: "[email protected]" };}const user = getUser(1);console.log(user.name);
선택적 프로퍼티
// 파일: src/optional-props.tsinterface CreateUserRequest { name: string; email: string; role?: string; // 없어도 됨 avatarUrl?: string; // 없어도 됨}function createUser(req: CreateUserRequest): void { const role = req.role ?? "viewer"; console.log(`Creating ${req.name} as ${role}`);}createUser({ name: "Alice", email: "[email protected]" });createUser({ name: "Bob", email: "[email protected]", role: "admin" });
선택적 프로퍼티에 접근할 때는 반드시 undefined 가능성을 처리해야 합니다.
읽기 전용 프로퍼티
// 파일: src/readonly-props.tsinterface Config { readonly host: string; readonly port: number; timeout: number; // 이건 변경 가능}const config: Config = { host: "localhost", port: 3000, timeout: 5000,};config.timeout = 10000; // OKconfig.host = "remote"; // Error: Cannot assign to 'host' because it is a read-only property
readonly는 불변 값임을 명시하는 문서 역할도 합니다. 특히 설정 객체, ID 필드에 씁니다.
interface 확장
// 파일: src/interface-extends.tsinterface Entity { id: number; createdAt: Date; updatedAt: Date;}interface User extends Entity { name: string; email: string;}interface AdminUser extends User { permissions: string[]; lastLogin: Date;}const admin: AdminUser = { id: 1, createdAt: new Date(), updatedAt: new Date(), name: "Alice", email: "[email protected]", permissions: ["read", "write", "delete"], lastLogin: new Date(),};
공통 필드를 베이스 인터페이스로 뽑아두면 중복이 줄고 일관성이 유지됩니다.
다중 확장
// 파일: src/multi-extends.tsinterface Timestamped { createdAt: Date; updatedAt: Date;}interface SoftDeletable { deletedAt: Date | null;}// 여러 인터페이스를 동시에 확장interface Post extends Timestamped, SoftDeletable { id: number; title: string; content: string;}
인덱스 시그니처
키 이름을 미리 알 수 없는 동적 객체에 씁니다.
// 파일: src/index-signature.tsinterface StringMap { [key: string]: string;}interface NumberDictionary { [key: string]: number; length: number; // OK — number이므로 인덱스 시그니처와 호환 name: string; // Error — 인덱스 시그니처가 number를 요구하는데 string}const headers: StringMap = { "Content-Type": "application/json", "Authorization": "Bearer token",};headers["X-Custom"] = "value"; // OK
인덱스 시그니처가 있으면 모든 명시적 프로퍼티도 그 타입을 따라야 합니다.
함수 프로퍼티와 메서드 시그니처
// 파일: src/method-signatures.tsinterface Logger { // 메서드 시그니처 log(message: string): void; warn(message: string): void; // 함수 프로퍼티 타입 (미묘한 차이 있음) error: (message: string) => void;}const consoleLogger: Logger = { log: (msg) => console.log(msg), warn: (msg) => console.warn(msg), error: (msg) => console.error(msg),};
두 방식 모두 동작하지만, 메서드 시그니처(log())는 선언 병합에서 오버로드가 추가되고, 함수 프로퍼티(log:)는 더 엄격한 strictFunctionTypes를 적용받습니다. 실무에서는 메서드 시그니처를 주로 씁니다.
정리
- 재사용 객체 타입은
interface로 정의합니다. extends로 공통 필드를 베이스로 분리하세요.- 변경되면 안 되는 필드는
readonly를 붙입니다. - 인덱스 시그니처는 동적 키 객체에만 씁니다.