[TypeScript] 제네릭 인터페이스

제네릭을 사용하는 이유

`any` 타입을 사용하면 모든 값을 허용할 수 있지만, 타입 안정성이 떨어집니다. 반면 제네릭은 호출 시점에 타입을 지정해, 코드의 의도를 명확히 하고 오류를 줄입니다. 제네릭은 한 함수나 인터페이스가 다양한 타입을 처리할 수 있도록 합니다. 예를 들어, 단순한 배열 처리 함수를 생각해 봅시다. 만약 숫자 배열과 문자열 배열에 대해 각각의 함수를 만들 필요 없이, 제네릭을 사용하면 하나의 함수로 모든 타입을 처리할 수 있습니다.

제네릭 함수 예제

function reverseList<T>(list: T[]): T[] {
  return list.reverse();
}

const numbers = reverseList<number>([1, 2, 3]);
const words = reverseList<string>(["가", "나", "다"]);

console.log(numbers); // 출력: [3, 2, 1]
console.log(words);   // 출력: ["다", "나", "가"]

`T`는 타입 파라미터로, 호출 시 지정된 타입에 따라 동작합니다.

제네릭 인터페이스

제네릭 인터페이스는 객체의 구조를 정의할 때, 타입 파라미터를 활용하여 유연하고 재사용 가능한 타입을 설계할 수 있게 해줍니다.

interface MessageStore<T> {
  content: T;
  timestamp: Date;
  getMessage(): T;
}

class SimpleStore<T> implements MessageStore<T> {
  constructor(public content: T, public timestamp = new Date()) {}

  getMessage(): T {
    return this.content;
  }
}

const textStore = new SimpleStore<string>("안녕하세요");
const countStore = new SimpleStore<number>(42);

console.log(textStore.getMessage());   // 출력: 안녕하세요
console.log(countStore.getMessage());  // 출력: 42

`MessageStore<T>`는 메시지와 타임스탬프를 관리하며, `T`로 다양한 타입을 처리합니다.

제네릭 인터페이스에 제약 적용하기

때로는 제네릭 타입이 특정 프로퍼티를 반드시 포함해야 하는 경우가 있습니다. 이럴 때는 extends 키워드를 사용해 제네릭 타입에 제약을 걸 수 있습니다.

interface Named {
  name: string;
}

interface Repository<T extends Named> {
  entries: T[];
  findByName(name: string): T | undefined;
}

class DataRepo<T extends Named> implements Repository<T> {
  constructor(public entries: T[]) {}

  findByName(name: string): T | undefined {
    return this.entries.find(entry => entry.name === name);
  }
}

const repo = new DataRepo<{ name: string; age: number }>([
  { name: "민수", age: 25 },
  { name: "지영", age: 30 },
]);

console.log(repo.findByName("지영")); // 출력: { name: "지영", age: 30 }

`T`는 `Named`를 확장해야 하므로 `name` 속성을 필수로 가집니다.