본문 바로가기
JavaScript/TypeScript

제네릭(Generic)

by curious week 2025. 6. 21.

연습하기: https://typescript-exercises.github.io/

 

TypeScript Exercises

A set of interactive TypeScript exercises

typescript-exercises.github.io


TypeScript의 제네릭(Generic) 타입


1. 제네릭이란?

"제네릭(Generic)"은 타입을 함수나 클래스, 인터페이스가 사용할 때, 외부에서 타입을 주입받도록 하는 기능입니다.

  • Java의 제네릭, C++의 템플릿 기능과 유사
  • 코드 재사용성과 타입 안정성을 동시에 확보 가능
  • 함수/클래스를 작성할 때 구체적인 타입을 지정하지 않고, 나중에 사용할 때 지정할 수 있음

2. 기본 문법

function identity<T>(value: T): T {
  return value;
}

// 사용
const num = identity<number>(10);      // T = number
const str = identity<string>('hello'); // T = string

여기서 T는 타입 변수(type variable)입니다. 타입 자리에 들어가는 매개변수 역할을 합니다.


3. 함수에서의 제네릭

function wrapInArray<T>(value: T): T[] {
  return [value];
}

const result = wrapInArray('hello'); // string[]

특징:

  • T가 자동 추론됨 (string)
  • 여러 타입을 묶고 싶으면 유니온 타입 or 다중 제네릭 사용 가능
function merge<T, U>(a: T, b: U): T & U {
  return { ...a, ...b };
}

merge({ name: 'Lee' }, { age: 30 }); // { name: string; age: number }

4. 제네릭 인터페이스

interface ApiResponse<T> {
  data: T;
  success: boolean;
}

const res: ApiResponse<string> = {
  data: 'ok',
  success: true,
};

실제 상황:

type User = { id: string; name: string };
const userRes: ApiResponse<User> = {
  data: { id: 'abc', name: 'Kim' },
  success: true,
};

5. 제네릭 클래스

class Box<T> {
  constructor(private value: T) {}
  getValue(): T {
    return this.value;
  }
}

const numberBox = new Box<number>(123);
numberBox.getValue(); // 123

6. 제네릭 제한 (Constraint)

T extends ...을 이용해 특정 조건을 걸 수 있습니다.

function printLength<T extends { length: number }>(arg: T): void {
  console.log(arg.length);
}

printLength('hello'); // OK
printLength([1, 2, 3]); // OK
// printLength(42);     // Error: number에는 length가 없음

7. 기본값 설정

function createMap<T = string>() {
  const map: Record<string, T> = {};
  return map;
}

const map = createMap();        // T는 string
const map2 = createMap<number>(); // T는 number

8. JSDoc에서 제네릭 주석

/**
 * @template T
 * @param {T} value - 입력값
 * @returns {T} 동일한 타입 반환
 */
function identity<T>(value) {
  return value;
}

TypeScript에서 JSDoc은 선택이지만, Typedoc으로 문서화 시 유용합니다.


9. 실전 예시: React Query에서 제네릭 사용

useQuery<User[]>({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

→ useQuery<T>는 내부적으로 T 타입의 data를 반환한다고 명시하는 방식입니다.


함수의 입력과 출력 타입이 동일할 때 function identity<T>(value: T): T
다양한 타입을 병합 function merge<T, U>(a: T, b: U): T & U
객체 속성 제한 필요 T extends { length: number }
기본값 필요 function create<T = string>()
API 응답 포맷 정의 interface ApiResponse<T>

TypeScript 고급 제네릭


1. keyof – 객체의 키를 유니온 타입으로 추출

type User = {
  id: string;
  name: string;
  age: number;
};

type UserKeys = keyof User; // "id" | "name" | "age"
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const u: User = { id: '1', name: 'Kim', age: 20 };
getValue(u, 'name'); // OK

2. typeof + keyof – 런타임 값을 타입으로 변환

const colors = {
  primary: '#fff',
  secondary: '#000',
};

type ColorKeys = keyof typeof colors; // "primary" | "secondary"

3. infer – 타입 추론 조건부 추출

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = () => string;
type R = ReturnType<Fn>; // string
  • infer R로 함수 반환값을 추출할 수 있음
  • 이건 타입 내부에서 타입을 "추측(infer)"하는 고급 문법

4. Mapped Type – 객체의 키를 돌며 타입 변환

type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

type PartialUser = {
  [K in keyof User]?: User[K];
};

→ 이건 TS 기본 제공 타입 Readonly<T>, Partial<T>로도 구현돼 있어요.


5. Conditional Type – 조건에 따라 타입 분기

type IsString<T> = T extends string ? true : false;

type A = IsString<'abc'>; // true
type B = IsString<123>;   // false

6. 실전 패턴: DTO 추출 유틸

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type OnlyStrings = PickByType<User, string>; 
// { id: string; name: string; }

7. 실전 패턴: DeepPartial (모든 필드를 재귀적으로 optional)

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type Config = {
  ui: {
    theme: string;
    layout: string;
  };
  server: {
    port: number;
  };
};

type PartialConfig = DeepPartial<Config>;

8. 실전 패턴: 함수 인자와 반환값 추출

type Args<T> = T extends (...args: infer A) => any ? A : never;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never;

const fn = (a: number, b: string): boolean => true;

type A = Args<typeof fn>; // [number, string]
type R = Ret<typeof fn>;  // boolean

9. 실전 패턴: 객체 키를 기반으로 유효한 Path 추출

type Path<T> = {
  [K in keyof T]: K extends string
    ? T[K] extends Record<string, any>
      ? K | `${K}.${Path<T[K]>}`
      : K
    : never;
}[keyof T];

type Deep = {
  user: {
    name: string;
    profile: {
      age: number;
    };
  };
};

type P = Path<Deep>; // "user" | "user.name" | "user.profile" | "user.profile.age"

10. 실전: API 응답 타입에서 DTO 생성

type ApiResponse<T> = {
  data: T;
  message: string;
};

type ExtractData<T> = T extends { data: infer D } ? D : never;

type UserApi = ApiResponse<User>;
type OnlyUser = ExtractData<UserApi>; // User

🔁 결합 예시

type Fn = (a: string, b: number) => boolean;

type ArgTypes = Args<Fn>;   // [string, number]
type Return = Ret<Fn>;      // boolean

자주 쓰이는 고급 제네릭 조합

객체 키 반복 처리 Mapped Types + keyof
조건에 따른 타입 분기 T extends U ? X : Y
함수 인자/리턴 타입 추출 infer
DTO 자동화 infer, Mapped, keyof
TS 유틸 타입 확장 Pick, Omit, Record, Exclude 등과 조합

 

'JavaScript > TypeScript' 카테고리의 다른 글

Jsdoc, ts, Typedoc  (6) 2025.06.21
Zod + zodResolver + React Hook Form  (0) 2025.05.15
TypeScript에서 타입을 정의하는 주요 방식  (1) 2025.05.02
Zod vs zod-validator  (1) 2025.03.19
Zod  (0) 2025.03.19