[TypeScript] 객체 타입 다루기

객체 타입 정의 방법

익명 객체 타입

함수의 매개변수 등에 직접 객체 타입을 명시할 수 있습니다.

function logInfo(data: { title: string; count: number }): void {
  console.log(`${data.title}: ${data.count}개`);
}

logInfo({ title: "책", count: 5 }); // 출력: 책: 5개

인터페이스 활용

재사용 가능한 객체 구조를 정의할 때 유용합니다.

interface Product {
  name: string;
  price: number;
}

const displayProduct = (prod: Product): string => `${prod.name} - ${prod.price}원`;
const item: Product = { name: "펜", price: 1000 };

console.log(displayProduct(item)); // 출력: 펜 - 1000원

타입 별칭 사용

객체 타입에 이름을 붙여 재사용성을 높입니다.

type Task = {
  id: number;
  description: string;
};

const addTask = (task: Task): void => {
  console.log(`Task #${task.id}: ${task.description}`);
};

const task1: Task = { id: 1, description: "회의 준비" };
addTask(task1); // 출력: Task #1: 회의 준비

속성 수정자

선택적 속성

`?`를 사용해 속성을 필수에서 선택으로 변경합니다.

interface Settings {
  theme?: string;
  fontSize?: number;
}

function applySettings(options: Settings): string {
  return `테마: ${options.theme ?? "기본"}, 크기: ${options.fontSize ?? 12}`;
}

console.log(applySettings({ theme: "다크" })); // 출력: 테마: 다크, 크기: 12
console.log(applySettings({}));                // 출력: 테마: 기본, 크기: 12

strictNullChecks 옵션이 활성화된 경우, 선택적 속성은 암묵적으로 undefined를 포함한 타입으로 취급됩니다.

읽기 전용 속성

속성을 읽기 전용으로 만들어, 객체 생성 후 변경되지 않도록 할 수 있습니다. `readonly`로 속성 변경을 막습니다.

interface Account {
  readonly accountId: string;
  balance: number;
}

const updateBalance = (acc: Account, amount: number): void => {
  acc.balance += amount;
  // acc.accountId = "new"; // 오류: 읽기 전용
};

const myAccount: Account = { accountId: "A123", balance: 500 };
updateBalance(myAccount, 200);
console.log(myAccount.balance); // 출력: 700

읽기 전용 속성은 객체의 참조 자체는 불변하지만, 내부 값(예: 객체 속성의 프로퍼티)은 변경될 수 있습니다. 

interface Account {
  readonly account: {
    id: string;
    name: string;
  }
  balance: number;
}

const updateBalance = (acc: Account, amount: number): void => {
  acc.balance += amount;
  // acc.account = { id: "B123", name: "soo" }; // 오류: 읽기 전용
  acc.account.name = "업데이트"
};

const myAccount: Account = { account: { id: "A123", name: "lee" }, balance: 500 };
updateBalance(myAccount, 200);
console.log(myAccount.account.name); // 출력: 업데이트

인덱스 시그니처

인덱스 시그니처를 사용하면, 미리 속성 이름을 알 수 없지만 모든 속성이 같은 타입을 가진 객체를 표현할 수 있습니다.

interface StringDictionary {
  [key: string]: string;
}

const translations: StringDictionary = {
  hello: "안녕하세요",
  goodbye: "안녕히 가세요",
};

console.log(translations["hello"]); // 출력: 안녕하세요

인덱스 시그니처를 선언하면, 객체의 모든 프로퍼티가 지정한 타입을 만족해야 합니다. 만약 일부 속성의 타입이 다르다면, 해당 타입들을 유니온으로 지정할 수 있습니다.

interface FlexibleDictionary {
  [key: string]: string | number;
  version: number; // OK
  appName: string; // OK
}

또한, 인덱스 시그니처에 readonly를 붙이면, 인덱스로 접근할 때 수정할 수 없도록 할 수 있습니다.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

const greetings: ReadonlyStringArray = ["Hi", "Hello", "Bonjour"];
// greetings[0] = "Hey"; // 오류: 읽기 전용 인덱스

과잉 속성 검사

객체 리터럴을 사용할 때, 지정된 타입에 없는 속성을 전달하면 TypeScript가 오류를 발생시킵니다.

interface Profile {
  name: string;
}

function saveProfile(profile: Profile): void {
  console.log(profile.name);
}

// saveProfile({ name: "하영", age: 25 }); // 오류: age는 정의되지 않음

// 해결: 타입 단언
saveProfile({ name: "하영", age: 25 } as Profile);

타입 확장과 결합

인터페이스 확장

`extends`로 기존 인터페이스를 확장합니다.

interface Contact {
  email: string;
}

interface Customer extends Contact {
  orders: number;
}

const client: Customer = { email: "a@b.com", orders: 3 };
console.log(client.email); // 출력: a@b.com

인터섹션 타입

`&`로 여러 타입을 결합합니다.

type Info = { id: number };
type Status = { active: boolean };

type UserStatus = Info & Status;

const userState: UserStatus = { id: 1, active: true };
console.log(userState); // 출력: { id: 1, active: true }

제네릭 객체 타입

제네릭을 사용하면, 객체 타입을 보다 재사용 가능하고 유연하게 정의할 수 있습니다.

interface Container<T> {
  value: T;
  label: string;
}

const textContainer: Container<string> = { value: "안녕", label: "인사" };
const numContainer: Container<number> = { value: 42, label: "숫자" };

console.log(textContainer.value); // 출력: 안녕
console.log(numContainer.value); // 출력: 42

읽기 전용 배열과 튜플

ReadonlyArray

배열을 수정하지 못하도록 하기 위해 readonly 배열을 사용할 수 있습니다.

function listItems(items: readonly string[]): void {
  console.log(items[0]);
  // items.push("추가"); // 오류
}

listItems(["a", "b"]);

Tuple 타입

튜플은 고정된 개수와 순서로 서로 다른 타입의 요소들을 포함할 수 있는 배열 타입입니다.

type StringNumberPair = [string, number];

function printPair(pair: StringNumberPair): void {
  const [text, num] = pair;
  console.log(`Text: ${text}, Number: ${num}`);
}

printPair(["hello", 42]);
// printPair(["hello"]); // 오류: 요소 부족
// printPair(["hello", 42, true]); // 오류: 요소 초과

변경 불가능한 Tuple

튜플에 readonly를 붙여 변경 불가능한 튜플로 만들 수도 있습니다.

const point = [3, 4] as const; // readonly [3, 4]
// point[0] = 10; // 오류: 읽기 전용 튜플