연습하기: 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 |