TypeScript 고급 타입 활용 가이드
소개
TypeScript는 정적 타입만으로도 버그를 많이 줄여 주지만, 제네릭·조건부 타입·매핑 타입까지 쓰면 API 설계나 공통 유틸을 더 안전하게 만들 수 있습니다. 우리가 대규모 코드베이스에서 타입을 활용할 때 자주 쓰는 패턴과, 실수하기 쉬운 부분을 중심으로 정리했습니다.
제네릭 (Generics)
제네릭은 타입을 매개변수화하여 재사용 가능한 컴포넌트를 만드는 핵심 기능입니다.
기본 제네릭 사용법
1// 제네릭 함수
2function identity<T>(arg: T): T {
3 return arg;
4}
5
6// 사용 예시
7const number = identity<number>(42);
8const string = identity<string>("hello");제네릭 제약 조건
1interface Lengthwise {
2 length: number;
3}
4
5function logLength<T extends Lengthwise>(arg: T): T {
6 console.log(arg.length);
7 return arg;
8}
9
10// length 속성이 있는 타입만 사용 가능
11logLength("hello"); // OK
12logLength([1, 2, 3]); // OK
13logLength(42); // Error: number에는 length 속성이 없음제네릭 클래스
1class GenericNumber<T> {
2 zeroValue: T;
3 add: (x: T, y: T) => T;
4}
5
6const myGenericNumber = new GenericNumber<number>();
7myGenericNumber.zeroValue = 0;
8myGenericNumber.add = (x, y) => x + y;유니온 타입과 인터섹션 타입
유니온 타입
1type StringOrNumber = string | number;
2
3function processValue(value: StringOrNumber) {
4 if (typeof value === "string") {
5 return value.toUpperCase();
6 } else {
7 return value.toFixed(2);
8 }
9}인터섹션 타입
1interface Person {
2 name: string;
3 age: number;
4}
5
6interface Employee {
7 employeeId: string;
8 department: string;
9}
10
11type EmployeePerson = Person & Employee;
12
13const employee: EmployeePerson = {
14 name: "John",
15 age: 30,
16 employeeId: "E001",
17 department: "Engineering"
18};조건부 타입 (Conditional Types)
조건부 타입은 타입 관계 검사에 따라 두 가지 타입 중 하나를 선택합니다.
기본 조건부 타입
1type IsArray<T> = T extends Array<any> ? true : false;
2
3type Test1 = IsArray<number[]>; // true
4type Test2 = IsArray<string>; // false분배 조건부 타입
1type ToArray<T> = T extends any ? T[] : never;
2
3type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]실전 예제: 함수 반환 타입 추출
1type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
2
3function getString(): string {
4 return "hello";
5}
6
7type StringReturn = ReturnType<typeof getString>; // string매핑 타입 (Mapped Types)
매핑 타입은 기존 타입을 기반으로 새로운 타입을 생성합니다.
기본 매핑 타입
1type Readonly<T> = {
2 readonly [P in keyof T]: T[P];
3};
4
5interface Person {
6 name: string;
7 age: number;
8}
9
10type ReadonlyPerson = Readonly<Person>;
11// { readonly name: string; readonly age: number; }Partial과 Required
1type Partial<T> = {
2 [P in keyof T]?: T[P];
3};
4
5type Required<T> = {
6 [P in keyof T]-?: T[P];
7};
8
9interface User {
10 id: number;
11 name: string;
12 email?: string;
13}
14
15type PartialUser = Partial<User>;
16// { id?: number; name?: string; email?: string; }
17
18type RequiredUser = Required<User>;
19// { id: number; name: string; email: string; }Pick과 Omit
1type Pick<T, K extends keyof T> = {
2 [P in K]: T[P];
3};
4
5type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
6
7interface User {
8 id: number;
9 name: string;
10 email: string;
11 password: string;
12}
13
14type PublicUser = Omit<User, 'password'>;
15// { id: number; name: string; email: string; }
16
17type UserId = Pick<User, 'id'>;
18// { id: number; }템플릿 리터럴 타입
템플릿 리터럴 타입은 문자열 리터럴 타입을 조합하여 새로운 타입을 만듭니다.
1type EventName<T extends string> = `on${Capitalize<T>}`;
2
3type ClickEvent = EventName<'click'>; // 'onClick'
4type ChangeEvent = EventName<'change'>; // 'onChange'
5
6// 실전 예제
7type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
8type ApiEndpoint = `/api/${string}`;
9
10type ApiCall = `${HttpMethod} ${ApiEndpoint}`;
11// 'GET /api/users' | 'POST /api/users' | ...타입 가드 (Type Guards)
타입 가드는 런타임에 타입을 좁혀주는 함수입니다.
1function isString(value: unknown): value is string {
2 return typeof value === 'string';
3}
4
5function processValue(value: unknown) {
6 if (isString(value)) {
7 // 이 블록에서 value는 string 타입
8 console.log(value.toUpperCase());
9 }
10}사용자 정의 타입 가드
1interface Cat {
2 type: 'cat';
3 meow: () => void;
4}
5
6interface Dog {
7 type: 'dog';
8 bark: () => void;
9}
10
11type Animal = Cat | Dog;
12
13function isCat(animal: Animal): animal is Cat {
14 return animal.type === 'cat';
15}
16
17function handleAnimal(animal: Animal) {
18 if (isCat(animal)) {
19 animal.meow();
20 } else {
21 animal.bark();
22 }
23}실전 활용 예제
API 응답 타입 처리
1type ApiResponse<T> =
2 | { success: true; data: T }
3 | { success: false; error: string };
4
5async function fetchUser(id: number): Promise<ApiResponse<User>> {
6 try {
7 const user = await getUserById(id);
8 return { success: true, data: user };
9 } catch (error) {
10 return { success: false, error: error.message };
11 }
12}
13
14// 사용
15const result = await fetchUser(1);
16if (result.success) {
17 console.log(result.data.name); // 타입 안전
18} else {
19 console.error(result.error);
20}유틸리티 타입 조합
1// 모든 속성을 선택적으로 만들고, 특정 속성은 제외
2type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
3
4interface User {
5 id: number;
6 name: string;
7 email: string;
8}
9
10// id는 필수, 나머지는 선택적
11type UpdateUser = PartialExcept<User, 'id'>;
12// { id: number; name?: string; email?: string; }결론
TypeScript의 고급 타입 기능들을 활용하면:
- 타입 안전성 향상: 컴파일 타임에 오류를 발견
- 코드 재사용성: 제네릭과 유틸리티 타입으로 중복 제거
- 가독성 향상: 타입 자체가 문서 역할
- 유지보수성: 타입 변경 시 영향 범위를 쉽게 파악
이러한 고급 타입 기능들을 적절히 활용하면 더 안전하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.