iBetter Books
수정

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

간단한 한 번짜리 타입은 인라인으로도 괜찮습니다. 재사용이 필요하면 interfacetype으로 빼세요.


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를 붙입니다.
  • 인덱스 시그니처는 동적 키 객체에만 씁니다.