iBetter Books
수정

Ch 01. 제네릭 함수 첫걸음

같은 로직인데 타입만 다른 함수를 여러 개 만들어본 적 있나요. 제네릭은 그 반복을 없애줍니다. 타입을 매개변수처럼 받아서, 하나의 함수가 다양한 타입에 대해 동작하도록 만듭니다.

왜 제네릭이 필요한가

배열에서 첫 번째 요소를 반환하는 함수를 만든다고 가정합니다.

// 파일: src/generics/motivation.ts// any 사용 — 타입 정보를 잃음function firstAny(arr: any[]): any {  return arr[0];}const result1 = firstAny([1, 2, 3]);// result1의 타입은 any — number인 것을 TypeScript가 모름result1.toFixed(2); // 오류 없음 (런타임 에러 위험)result1.toUpperCase(); // 오류 없음 (런타임 에러 위험)// 타입별 함수 작성 — 중복 발생function firstNumber(arr: number[]): number {  return arr[0];}function firstString(arr: string[]): string {  return arr[0];}

제네릭으로 이 문제를 해결합니다.

// 파일: src/generics/first-generic.tsfunction first<T>(arr: T[]): T | undefined {  return arr[0];}const num = first([1, 2, 3]);       // num: number | undefinedconst str = first(["a", "b", "c"]); // str: string | undefinedconst bool = first([true, false]);  // bool: boolean | undefined

T는 타입 매개변수입니다. 함수를 호출할 때 TypeScript가 인자를 보고 T를 자동으로 추론합니다. first([1, 2, 3])을 보고 T = number로 추론하는 방식입니다.

타입 매개변수 명시적으로 전달하기

추론이 불가능하거나 원하는 타입을 강제하고 싶을 때 명시적으로 전달합니다.

// 파일: src/generics/explicit-type.tsfunction createArray<T>(length: number, defaultValue: T): T[] {  return Array(length).fill(defaultValue);}// 추론 — T = stringconst strings = createArray(3, "hello"); // string[]// 명시적 — T = numberconst numbers = createArray<number>(5, 0); // number[]// 빈 배열은 추론 불가 — 명시 필요const empty = createArray<boolean>(3, false); // boolean[]

여러 타입 매개변수 사용하기

함수가 서로 다른 타입 두 개를 다룰 때 매개변수도 두 개 씁니다.

// 파일: src/generics/multiple-type-params.tsfunction pair<A, B>(first: A, second: B): [A, B] {  return [first, second];}const p1 = pair(1, "hello");          // [number, string]const p2 = pair(true, { id: 1 });     // [boolean, { id: number }]// 객체로 묶기function zip<K extends string, V>(key: K, value: V): Record<K, V> {  return { [key]: value } as Record<K, V>;}const obj = zip("name", "Alice"); // { name: string }

함수 타입에서 제네릭 활용하기

// 파일: src/generics/transform.tsfunction map<T, U>(arr: T[], fn: (item: T) => U): U[] {  return arr.map(fn);}const doubled = map([1, 2, 3], (n) => n * 2);           // number[]const lengths = map(["hello", "world"], (s) => s.length); // number[]const parsed = map(["1", "2", "3"], Number);             // number[]function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {  return arr.filter(predicate);}const evens = filter([1, 2, 3, 4, 5], (n) => n % 2 === 0); // number[]

제네릭 vs any — 핵심 차이

// 파일: src/generics/generic-vs-any.ts// any: 타입 정보 완전히 소실function identityAny(arg: any): any {  return arg;}const result = identityAny(42);// result의 타입: any// 어떤 메서드든 호출해도 컴파일 오류 없음 (위험)// 제네릭: 타입 관계 보존function identity<T>(arg: T): T {  return arg;}const num = identity(42);// num의 타입: number// number 메서드만 호출 가능// 인자와 반환값의 타입이 같음을 보장function swap<T, U>(pair: [T, U]): [U, T] {  return [pair[1], pair[0]];}const swapped = swap([1, "hello"]); // [string, number]// swapped[0]: string, swapped[1]: number — 정확하게 추론됨

any는 "이 값의 타입을 모르겠다"고 말하는 것입니다. 제네릭은 "이 값의 타입은 호출자가 결정한다"고 말하는 것입니다. 제네릭은 타입 정보를 잃지 않으면서 유연성을 얻습니다.

화살표 함수에서 제네릭 작성하기

화살표 함수에서 제네릭을 쓸 때 TypeScript 파일과 TSX 파일에서 문법이 약간 다릅니다.

// 파일: src/generics/arrow-generic.ts// .ts 파일 — 일반적인 문법const identity = <T>(arg: T): T => arg;// .tsx 파일에서는 JSX와 혼동 방지를 위해 쉼표를 추가하거나// extends를 사용합니다const identity2 = <T,>(arg: T): T => arg;const identity3 = <T extends unknown>(arg: T): T => arg;

제네릭 함수의 핵심은 "타입 정보를 보존하면서 재사용 가능한 코드를 작성하는 것"입니다. 배열 순회, 변환, 래핑 같은 범용 로직을 제네릭으로 작성하면 타입 안전성을 유지하면서 중복을 줄일 수 있습니다.