TypeScript

[TypeScript] 제네릭(Generic) 응용

2025년 11월 29일
1
TypeScriptStudyGenericsPromise

📑 목차

지난 포스팅에서 제네릭의 기본 개념(T를 변수처럼 사용)을 익혔다. 이번에는 다양한 상황에서 제네릭을 어떻게 응용할 수 있는지 알아본다.

1. 제네릭 함수 응용

1-1. 다중 타입 변수 (T, U)

필요하다면 타입 변수를 2개 이상 선언해서 사용할 수 있다.

function swap<T, U>(a: T, b: U) {
  return [b, a];
}

const [a, b] = swap("1", 2);
// a는 number, b는 string 타입으로 자동 추론됨

1-2. 배열 타입 다루기 (T[])

배열의 요소 타입을 제네릭으로 받으면, 다양한 타입의 배열을 처리하는 함수를 만들 수 있다.

function returnFirstValue<T>(data: T[]) {
  return data[0];
}

let num = returnFirstValue([0, 1, 2]); // T는 number, 반환값 number
let str = returnFirstValue([1, "hello", "mynameis"]); // T는 number | string

💡 튜플로 첫 번째 요소 타입 확정하기 만약 배열의 첫 번째 요소 타입을 정확히 추론하고 싶다면 튜플과 나머지 파라미터를 활용한다.

function returnFirstValue<T>(data: [T, ...unknown[]]) {
  return data[0];
}

let str = returnFirstValue([1, "hello", "mynameis"]); 
// 반환값이 정확히 number로 추론됨 (유니온 타입 아님)

1-3. 타입 변수 제한하기 (extends)

타입 변수에 아무 타입이나 들어오는 것을 막고, 특정 조건을 만족하는 타입만 받도록 제한할 수 있다.

// length 프로퍼티가 있는 타입만 허용
function getLength<T extends { length: number }>(data: T) {
  return data.length;
}

getLength("123");         // ✅ (string은 length 있음)
getLength([1, 2, 3]);     // ✅ (array는 length 있음)
getLength({ length: 1 }); // ✅ (객체에 length 있음)
// getLength(undefined);  // ❌ (length 없음)

2. map & forEach 메서드 직접 구현하기

자바스크립트의 내장 메서드인 mapforEach도 제네릭을 이용해 타입을 정의할 수 있다.

map 메서드 타입 정의

map은 원본 배열의 타입(T)과 변환된 배열의 타입(U)이 다를 수 있으므로, 두 개의 타입 변수가 필요하다.

function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

const arr = [1, 2, 3];
const result = map(arr, (it) => it.toString());
// result는 string[] 타입 ["1", "2", "3"]

forEach 메서드 타입 정의

forEach는 반환값이 없으므로(void), 비교적 간단하게 정의할 수 있다.

function forEach<T>(arr: T[], callback: (item: T) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

3. 제네릭 인터페이스 & 타입 별칭

3-1. 제네릭 인터페이스

인터페이스 이름 뒤에 <T> 등을 붙여 정의한다.

interface KeyPair<K, V> {
  key: K;
  value: V;
}

// ⚠️ 주의: 변수 정의 시 반드시 타입을 명시해야 함
let keyPair: KeyPair<string, number> = {
  key: "key",
  value: 0,
};

3-2. 인덱스 시그니처와 함께 사용

interface Map<V> {
  [key: string]: V;
}

let booleanMap: Map<boolean> = {
  key: true,
};

3-3. 제네릭 타입 별칭

인터페이스와 동일하게 사용 가능하다.

type Map2<V> = {
  [key: string]: V;
};

4. 실전 예제: 제네릭 인터페이스 활용

유저의 프로필(profile)이 상황에 따라 Student일 수도 있고 Developer일 수도 있다면? 제네릭을 이용해 중복 코드를 없애고 타입 안정성을 높일 수 있다.

interface Student {
  type: "student";
  school: string;
}

interface Developer {
  type: "developer";
  skill: string;
}

// 제네릭 인터페이스 정의
interface User<T> {
  name: string;
  profile: T;
}

// 학생만 이용 가능한 함수: 매개변수 타입을 User<Student>로 제한
function goToSchool(user: User<Student>) {
  const school = user.profile.school; // 타입 좁히기 없이 바로 접근 가능!
  console.log(`${school}로 등교 완료`);
}

함수 내부에서 if문으로 타입을 좁힐 필요가 없어져 코드가 훨씬 깔끔해진다.


5. 제네릭 클래스

클래스도 제네릭을 사용하면 하나의 클래스로 다양한 타입의 데이터를 처리할 수 있다.

class List<T> {
  constructor(private list: T[]) {}

  push(data: T) {
    this.list.push(data);
  }
  
  print() {
    console.log(this.list);
  }
}

// 생성자 인수로 타입 추론 가능
const numberList = new List([1, 2, 3]); // T는 number
const stringList = new List(["a", "b"]); // T는 string

제네릭이 없다면 NumberList, StringList 클래스를 따로 만들어야 했을 것이다.


6. 프로미스(Promise)와 제네릭

비동기 처리에 사용되는 Promise는 제네릭 클래스로 구현되어 있다. 성공했을 때(resolve) 반환되는 값의 타입을 제네릭으로 지정해 줄 수 있다.

// 1. 생성 시점에 타입 지정
const promise = new Promise<number>((resolve, reject) => {
  setTimeout(() => {
    resolve(20);
  }, 3000);
});

promise.then((response) => {
  // response는 number 타입 (20)
  console.log(response);
});

함수의 반환값으로 사용할 때는 다음과 같이 명시하는 것이 직관적이다.

interface Post {
  id: number;
  title: string;
  content: string;
}

// 반환값 타입에 Promise<Post> 명시
function fetchPost(): Promise<Post> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        id: 1,
        title: "게시글 제목",
        content: "게시글 본문",
      });
    }, 3000);
  });
}

참고: reject로 전달되는 에러 값의 타입은 정의할 수 없으며, 기본적으로 unknown (또는 any)으로 처리된다.


요약

  1. 함수 응용: 다중 타입 변수, 배열 처리, extends를 이용한 타입 제한 등 다양하게 활용 가능.
  2. 인터페이스/타입 별칭: 객체의 프로퍼티 타입을 유연하게 변경하며 재사용성 극대화.
  3. 클래스: 하나의 클래스로 여러 타입의 데이터를 처리하는 범용 클래스 생성.
  4. Promise: 비동기 작업의 결과값 타입을 제네릭으로 정의하여 안전하게 처리.
@taemni

@taemni

안녕하세요, 차근차근 성장 중인 조태민입니다.

instagram

댓글