Generic 완전 정복
Generic은 타입을 매개변수처럼 다루는 TypeScript의 핵심 기능입니다. 함수, 인터페이스, 클래스에서 타입의 재사용성과 안전성을 동시에 확보할 수 있습니다. 여기서는 기본 문법부터 실전 패턴까지 단계적으로 정리합니다.
Generic을 써야 하는 이유
섹션 제목: “Generic을 써야 하는 이유”Union 타입으로 해결하려 하면 인수로 허용되는 타입이 너무 광범위해집니다. Generic을 사용하면 호출 시점에 타입이 좁혀져 정확한 추론이 가능합니다.
interface Animal { name: string;}
interface Human { firstName: string; lastName: string;}
const getDisplayName = <TItem extends Animal | Human>( item: TItem): TItem extends Human ? { display: Human["firstName"] } : { display: Animal["name"] } => { if ("name" in item) { return { display: item.name }; } return { display: item.firstName };};TItem이 Human인지 Animal인지에 따라 반환 타입이 정확하게 추론됩니다.
extends로 Generic 제약하기
섹션 제목: “extends로 Generic 제약하기”extends를 사용하면 Generic에 들어올 수 있는 타입을 제한할 수 있습니다. 중첩된 객체의 깊은 값에 타입 안전하게 접근하는 예시입니다.
export const getDeepValue = < Obj, FirstKey extends keyof Obj, SecondKey extends keyof Obj[FirstKey]>( obj: Obj, firstKey: FirstKey, secondKey: SecondKey): Obj[FirstKey][SecondKey] => { return {} as any;};
const obj = { foo: { a: true, b: 2 }, bar: { c: false, d: 4 },};
const result = getDeepValue(obj, "bar", "c");// 자동완성 지원 + 타입 추론 완벽동적 함수 인자 (Dynamic Function Arguments)
섹션 제목: “동적 함수 인자 (Dynamic Function Arguments)”Generic과 조건부 타입을 조합하면, 이벤트 타입에 따라 payload 유무를 동적으로 결정할 수 있습니다.
const sendEvent = <Type extends Event["type"]>( ...args: Extract<Event, { type: Type }> extends { payload: infer TPayload } ? [type: Type, payload: TPayload] : [type: Type]) => {};핵심 개념:
Type은Event["type"]의 멤버 중 하나로 제한됩니다.Extract로 해당 타입의 이벤트를 추출한 뒤,payload필드가 있으면 두 번째 인자를 요구합니다.- 타입은 집합의 관점으로 이해해야 합니다. 더 구체적일수록 subtype, 제한이 적을수록 supertype입니다.
React 컴포넌트에서 Generic 활용
섹션 제목: “React 컴포넌트에서 Generic 활용”Generic 컴포넌트를 만들면 props 타입을 동적으로 추론할 수 있습니다.
interface TableProps<TItem> { items: TItem[]; renderItem: (item: TItem) => React.ReactNode;}
export function Table<TItem>(props: TableProps<TItem>) { return null;}
const Comp = () => { return ( <Table items={[{ id: "1" }]} renderItem={(item) => <div>{item.id}</div>} /> );};items에 전달한 배열의 타입이 renderItem의 item 매개변수로 자동 추론됩니다.
주의할 점: 이 패턴에서는 renderItem이 React 엘리먼트가 아닌 함수(렌더 프롭)로 전달됩니다. Table이 렌더링될 때마다 자식 요소도 함께 렌더링되므로, 불필요한 리렌더링을 방지하려면 memo와 useMemo를 활용한 메모이제이션이 필요합니다.
Generic Slots: 클로저처럼 동작하는 타입
섹션 제목: “Generic Slots: 클로저처럼 동작하는 타입”Generic도 클로저처럼 바깥 스코프의 타입 매개변수를 기억할 수 있습니다.
export const makeKeyRemover = <Key extends string>(keys: Key[]) => <Obj>(obj: Obj): Omit<Obj, Key> => { return {} as any; };
const keyRemover = makeKeyRemover(["a", "b"]);const newObject = keyRemover({ a: 1, b: 2, c: 3 });// newObject의 타입: { c: number }makeKeyRemover를 호출할 때 Key가 결정되고, 반환된 함수를 호출할 때 Obj가 결정됩니다. 이처럼 Generic의 추론 시점을 분리하면 더 유연한 API를 설계할 수 있습니다.