제네릭 제약 — extends로 타입 매개변수 제한하기
제네릭 제약(Generic Constraint)은
extends를 사용해서 타입 매개변수가 가져야 할 최소한의 조건 을 지정하는 것입니다.
왜 제약이 필요한가
// ❌ 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로 제약 걸기
// 인터페이스로 제약
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 제약
객체의 키만 허용하도록 제약할 때 사용합니다.
// 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와 인덱스 접근 타입
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
다중 제약
여러 조건을 동시에 만족해야 할 때 인터섹션을 사용합니다.
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가 없음
제네릭과 클래스 제약
// 생성자 함수를 받는 제네릭
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는 제네릭 제약뿐 아니라 조건부 타입 에서도 사용됩니다 (별도 편에서 상세히 다룸).
// 조건부 타입 미리보기
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
실전 패턴: 제네릭 API 함수
// 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[]
제네릭 제약의 일반적인 실수
// ❌ 제약을 반환 타입으로 사용
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형태로 생성자 함수에도 제약을 걸 수 있다
댓글 로딩 중...