제네릭 제약(Generic Constraint)은 extends를 사용해서 타입 매개변수가 가져야 할 최소한의 조건 을 지정하는 것입니다.

왜 제약이 필요한가

TYPESCRIPT
// ❌ T가 어떤 타입인지 몰라서 .length에 접근 불가
function getLength<T>(value: T): number {
  // return value.length; // Error: Property 'length' does not exist on type 'T'
  return 0;
}

// ✅ T는 length 속성이 있는 타입이어야 한다
function getLengthSafe<T extends { length: number }>(value: T): number {
  return value.length; // OK
}

getLengthSafe('hello');     // OK — string에 length가 있음
getLengthSafe([1, 2, 3]);   // OK — 배열에 length가 있음
// getLengthSafe(42);       // ❌ Error — number에 length가 없음

extends로 제약 걸기

TYPESCRIPT
// 인터페이스로 제약
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

// HasId를 확장한 타입은 모두 사용 가능
interface User extends HasId {
  name: string;
}

interface Product extends HasId {
  title: string;
  price: number;
}

const users: User[] = [{ id: 1, name: '홍길동' }];
const products: Product[] = [{ id: 1, title: '키보드', price: 50000 }];

const user = findById(users, 1);       // User | undefined
const product = findById(products, 1); // Product | undefined

keyof 제약

객체의 키만 허용하도록 제약할 때 사용합니다.

TYPESCRIPT
// T의 키만 K로 허용
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: '홍길동', age: 25, email: 'test@test.com' };

const name = getProperty(user, 'name');   // string
const age = getProperty(user, 'age');     // number
// getProperty(user, 'phone');            // ❌ Error — 'phone'은 user의 키가 아님

면접에서 keyof와 제네릭 제약을 결합한 getProperty 함수를 직접 작성하라는 문제가 자주 나옵니다.

keyof와 인덱스 접근 타입

TYPESCRIPT
type User = {
  name: string;
  age: number;
  email: string;
};

// keyof User = 'name' | 'age' | 'email'
type UserKeys = keyof User;

// T[K]로 특정 키의 값 타입을 가져옴
type NameType = User['name'];     // string
type AgeType = User['age'];       // number

다중 제약

여러 조건을 동시에 만족해야 할 때 인터섹션을 사용합니다.

TYPESCRIPT
interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

// T는 name과 age를 모두 가져야 함
function greet<T extends HasName & HasAge>(person: T): string {
  return `${person.name}님은 ${person.age}세입니다.`;
}

greet({ name: '홍길동', age: 25 });               // OK
greet({ name: '홍길동', age: 25, email: 'test' }); // OK — 추가 속성은 무관
// greet({ name: '홍길동' });                      // ❌ Error — age가 없음

제네릭과 클래스 제약

TYPESCRIPT
// 생성자 함수를 받는 제네릭
function createInstance<T>(Constructor: new () => T): T {
  return new Constructor();
}

class Dog {
  bark() { return '멍멍'; }
}

const dog = createInstance(Dog); // Dog
dog.bark(); // OK

// 매개변수가 있는 생성자
function createWithArgs<T>(
  Constructor: new (...args: any[]) => T,
  ...args: any[]
): T {
  return new Constructor(...args);
}

조건부 타입에서의 extends

extends는 제네릭 제약뿐 아니라 조건부 타입 에서도 사용됩니다 (별도 편에서 상세히 다룸).

TYPESCRIPT
// 조건부 타입 미리보기
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

실전 패턴: 제네릭 API 함수

TYPESCRIPT
// API 함수를 제네릭으로 만들어 재사용
interface ApiResponse<T> {
  data: T;
  status: number;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { data: data as T, status: response.status };
}

// 호출 시 반환 타입을 지정
interface User { id: number; name: string; }
const { data } = await fetchApi<User>('/api/user/1');
// data: User

interface Product { id: number; title: string; }
const { data: products } = await fetchApi<Product[]>('/api/products');
// products: Product[]

제네릭 제약의 일반적인 실수

TYPESCRIPT
// ❌ 제약을 반환 타입으로 사용
function first<T extends any[]>(arr: T): T[0] {
  return arr[0]; // T[0]은 배열의 첫 번째 요소 타입
}

// ✅ 더 좋은 방법
function firstBetter<T>(arr: T[]): T | undefined {
  return arr[0];
}

// ❌ 불필요한 제약
function identity<T extends unknown>(value: T): T {
  return value; // extends unknown은 모든 타입이므로 의미 없음
}

// ✅ 제약 없이도 동일
function identityBetter<T>(value: T): T {
  return value;
}

정리

  • extends로 제네릭 타입 매개변수에 최소 조건을 지정한다
  • keyof와 결합하면 객체의 키만 허용하는 안전한 접근이 가능하다
  • 다중 제약은 &(인터섹션)으로 조합한다
  • 제네릭 제약은 "이 타입은 최소한 이런 구조를 가져야 한다"를 의미한다
  • new () => T 형태로 생성자 함수에도 제약을 걸 수 있다
댓글 로딩 중...