콘텐츠로 이동

Generic 완전 정복

Generic은 타입을 매개변수처럼 다루는 TypeScript의 핵심 기능입니다. 함수, 인터페이스, 클래스에서 타입의 재사용성과 안전성을 동시에 확보할 수 있습니다. 여기서는 기본 문법부터 실전 패턴까지 단계적으로 정리합니다.

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 };
};

TItemHuman인지 Animal인지에 따라 반환 타입이 정확하게 추론됩니다.

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]
) => {};

핵심 개념:

  • TypeEvent["type"]의 멤버 중 하나로 제한됩니다.
  • Extract로 해당 타입의 이벤트를 추출한 뒤, payload 필드가 있으면 두 번째 인자를 요구합니다.
  • 타입은 집합의 관점으로 이해해야 합니다. 더 구체적일수록 subtype, 제한이 적을수록 supertype입니다.

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에 전달한 배열의 타입이 renderItemitem 매개변수로 자동 추론됩니다.

주의할 점: 이 패턴에서는 renderItem이 React 엘리먼트가 아닌 함수(렌더 프롭)로 전달됩니다. Table이 렌더링될 때마다 자식 요소도 함께 렌더링되므로, 불필요한 리렌더링을 방지하려면 memouseMemo를 활용한 메모이제이션이 필요합니다.

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를 설계할 수 있습니다.