TypeScript 제네릭 — 타입을 "매개변수"처럼 끼워 넣기
라이브러리 타입 정의에 나오는 <T>가 늘 벽이었다. 제네릭을 "로직은 하나로 두고 타입만 갈아 끼우는 빈칸"으로 이해하니, useQuery<User[]> 같은 문법이 읽히기 시작했다. 퍼블리셔 시선으로 정리한다.
TypeScript를 읽다 보면 <T>가 붙는 순간 문장이 갑자기 안 읽혀요.
useQuery<User[]>, ApiResponse<T>… 저도 오래 이 기호 앞에서 멈칫했어요.
알고 보니 제네릭은 퍼블리셔가 매일 하던 일이었어요.
카드 컴포넌트 하나 만들어두고 안의 내용물만 바꿔 재사용하던 것, 기억나시죠.
제네릭이 딱 그거예요.
로직(마크업)은 하나로 두고, 들어오는 타입만 바꿔 끼우는 문법.
<T>는 “여기에 나중에 타입이 들어올 자리”라는 빈칸 표시예요.
함수를 호출하는 순간 TypeScript가 인자를 보고 그 빈칸을 자동으로 채워요(타입 추론).
// T는 "나중에 정해질 타입"의 자리표시
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined ← T=number로 추론
const s = first(['a', 'b']); // s: string | undefined ← T=string으로 추론
이득은 any처럼 타입 정보를 버리지 않으면서도, 타입마다 함수를 복붙하지 않아도 된다는 거예요.
any로 뒀으면 반환값 타입이 사라졌을 텐데, <T> 덕에 넣은 타입이 그대로 따라 나오죠.
실무에서 제일 자주 보는 건 API 공통 응답 래퍼예요.
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserRes = ApiResponse<{ id: number; name: string }>;
// data 부분만 갈아 끼우면 모든 API 응답 타입이 완성된다
겉껍데기(status, message)는 늘 같고 data만 바뀌니까, 그 data 자리를 <T> 빈칸으로 두는 거예요.
이 하나로 모든 엔드포인트의 응답 타입을 찍어낼 수 있어요.
useQuery<User[]>, ShadCN 컴포넌트의 props, 공통 API 레이어의 응답 타입 — 전부 이 문법이에요.
읽을 줄 알면 그동안 암호 같던 라이브러리 타입 에러 메시지가 해석되기 시작해요.
남의 타입을 빨리 읽는 건 실무에서 생각보다 큰 무기고요.
헷갈렸던 것 두 가지만 적어둘게요.
<T>는 선언부에만 있는 자리표시예요.
function wrap<T>(v: T): T[]를 wrap('hi')로 부르면 반환 타입은 T[]가 아니라 string[]이에요.
호출 순간 T가 string으로 정해져 빈칸에 대입되니까요.
그리고 제네릭 인수는 기본적으로 필수예요.
ApiResponse<T>를 그냥 ApiResponse라고 쓰면 “1 type argument required” 에러가 나요.
인수 없이 쓰려면 함수 매개변수 기본값처럼 기본 타입을 줍니다.
interface ApiResponse<T = unknown> { /* ... */ }
// 이제 ApiResponse만 써도 T가 unknown으로 채워진다
<T>를 볼 때마다 “여기 나중에 타입이 들어올 빈칸이구나”로 읽으면 돼요.
퍼블리싱에서 슬롯에 내용물 넣던 그 감각 그대로.
이 제네릭으로 TypeScript가 미리 만들어둔 게 유틸리티 타입이고요.
댓글