Frontend

TypeScript 고급 타입 활용 가이드

TypeScript의 고급 타입 기능들을 활용하여 더 안전하고 유연한 코드를 작성하는 방법을 알아봅니다. 제네릭, 조건부 타입, 매핑 타입 등을 실전 예제와 함께 설명합니다.

TypeScript 고급 타입 활용 가이드

소개

TypeScript는 정적 타입만으로도 버그를 많이 줄여 주지만, 제네릭·조건부 타입·매핑 타입까지 쓰면 API 설계나 공통 유틸을 더 안전하게 만들 수 있습니다. 우리가 대규모 코드베이스에서 타입을 활용할 때 자주 쓰는 패턴과, 실수하기 쉬운 부분을 중심으로 정리했습니다.

제네릭 (Generics)

제네릭은 타입을 매개변수화하여 재사용 가능한 컴포넌트를 만드는 핵심 기능입니다.

기본 제네릭 사용법

typescript
1// 제네릭 함수
2function identity<T>(arg: T): T {
3  return arg;
4}
5
6// 사용 예시
7const number = identity<number>(42);
8const string = identity<string>("hello");

제네릭 제약 조건

typescript
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 속성이 없음

제네릭 클래스

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

유니온 타입과 인터섹션 타입

유니온 타입

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

인터섹션 타입

typescript
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)

조건부 타입은 타입 관계 검사에 따라 두 가지 타입 중 하나를 선택합니다.

기본 조건부 타입

typescript
1type IsArray<T> = T extends Array<any> ? true : false;
2
3type Test1 = IsArray<number[]>; // true
4type Test2 = IsArray<string>; // false

분배 조건부 타입

typescript
1type ToArray<T> = T extends any ? T[] : never;
2
3type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

실전 예제: 함수 반환 타입 추출

typescript
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)

매핑 타입은 기존 타입을 기반으로 새로운 타입을 생성합니다.

기본 매핑 타입

typescript
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

typescript
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

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

템플릿 리터럴 타입

템플릿 리터럴 타입은 문자열 리터럴 타입을 조합하여 새로운 타입을 만듭니다.

typescript
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)

타입 가드는 런타임에 타입을 좁혀주는 함수입니다.

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

사용자 정의 타입 가드

typescript
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 응답 타입 처리

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

유틸리티 타입 조합

typescript
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의 고급 타입 기능들을 활용하면:

  1. 타입 안전성 향상: 컴파일 타임에 오류를 발견
  2. 코드 재사용성: 제네릭과 유틸리티 타입으로 중복 제거
  3. 가독성 향상: 타입 자체가 문서 역할
  4. 유지보수성: 타입 변경 시 영향 범위를 쉽게 파악

이러한 고급 타입 기능들을 적절히 활용하면 더 안전하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

참고 자료

공유하기

관련 포스트