개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

제네릭 — 타입을 변수처럼 다루기

2025년 11월 19일
TypeScriptGeneric타입재사용성

숫자 배열의 첫 번째 원소를 반환하는 함수를 만들었습니다. 나중에 문자열 배열에도 같은 함수가 필요해졌습니다. any를 쓰면 타입 안전성을 잃고, 함수를 복제하면 유지보수가 어렵습니다. 제네릭은 이 문제의 해답입니다.

제네릭이 없다면

// any: 타입 안전성 없음
function firstAny(arr: any[]): any {
  return arr[0];
}

const result = firstAny([1, 2, 3]);
result.toUpperCase(); // 런타임 오류 — TypeScript가 number임을 모름

// 타입별 중복 함수: 코드 중복
function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }

제네릭 기본 문법

타입 매개변수를 <T>로 선언합니다. T는 함수가 호출될 때 실제 타입으로 채워집니다.

function first<T>(arr: T[]): T {
  return arr[0];
}

const n = first([1, 2, 3]);    // T = number, 반환 타입 number
const s = first(["a", "b"]);   // T = string, 반환 타입 string

n.toFixed(2);     // 정상 — n이 number임을 알고 있음
s.toUpperCase();  // 정상 — s가 string임을 알고 있음

TypeScript가 인수에서 T를 추론하므로, 대부분의 경우 명시적으로 타입을 지정하지 않아도 됩니다.

first<number>([1, 2, 3]); // 명시 — 가능하지만 불필요
first([1, 2, 3]);          // 추론 — 권장

타입 매개변수(Type Parameter): 제네릭에서 실제 타입 대신 쓰는 자리 표시자. 관례상 T(Type), K(Key), V(Value), E(Element)처럼 대문자 한 글자를 씁니다.

여러 타입 매개변수

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const entry = pair("name", "철수");  // [string, string]
const entry2 = pair(1, true);        // [number, boolean]

제네릭 인터페이스

인터페이스에도 타입 매개변수를 사용할 수 있습니다.

interface Box<T> {
  value: T;
  getValue(): T;
}

const stringBox: Box<string> = {
  value: "안녕",
  getValue() { return this.value; },
};

const numberBox: Box<number> = {
  value: 42,
  getValue() { return this.value; },
};

제네릭 클래스

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.pop(); // number | undefined

타입 제약 (extends)

타입 매개변수에 제약을 걸어, 특정 구조를 가진 타입만 받도록 할 수 있습니다.

// T는 length 프로퍼티가 있는 타입이어야 함
function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("안녕하세요"); // 5
getLength([1, 2, 3]);   // 3
getLength(42);           // 오류: number에는 length가 없습니다

keyof와 함께 사용

객체에서 존재하는 키만 허용하도록 제약을 걸 수 있습니다.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "철수", age: 30 };

getProperty(user, "name"); // "철수" (string)
getProperty(user, "age");  // 30 (number)
getProperty(user, "email"); // 오류: "email"은 User의 키가 아닙니다

기본 타입 매개변수

타입 매개변수에 기본값을 설정할 수 있습니다.

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

const generic: ApiResponse = { data: "뭐든지", status: 200 };  // T = unknown
const typed: ApiResponse<string> = { data: "문자열", status: 200 };

실전 예시 — API 유틸리티

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as T;
}

interface User {
  id: number;
  name: string;
}

const user = await fetchData<User>("/api/users/1");
user.name; // string — 타입 안전하게 사용 가능

제네릭은 코드 재사용성과 타입 안전성을 동시에 잡는 도구입니다. any를 쓰고 싶은 순간이 오면 먼저 제네릭으로 해결할 수 있는지 검토해 보는 것이 좋습니다.

← 이전 글Narrowing — 타입 범위를 좁히는 방법
다음 글 →유틸리티 타입 — 기존 타입을 변환하는 내장 도구