개요

TypeScript 5.x는 데코레이터 표준화, const 타입 매개변수, 향상된 타입 추론 등 강력한 기능들을 도입했습니다. 이 가이드에서는 TypeScript 5의 주요 신기능과 고급 타입 시스템 활용법을 다룹니다.

TypeScript 5.x 주요 신기능

1. 데코레이터 (Stage 3 표준)

TypeScript 5.0부터 ECMAScript 표준 데코레이터를 지원합니다:

function logged(target: Function, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);

  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with args:`, args);
    const result = target.call(this, ...args);
    console.log(`Result:`, result);
    return result;
  };
}

class Calculator {
  @logged
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);
// 로그: Calling add with args: [2, 3]
// 로그: Result: 5

2. const 타입 매개변수

함수 호출 시 리터럴 타입을 보존할 수 있습니다:

// 이전 방식
function createConfig<T>(config: T) {
  return config;
}

const config1 = createConfig({ mode: 'development' });
// 타입: { mode: string }

// TypeScript 5.0+ const 타입 매개변수
function createConfig<const T>(config: T) {
  return config;
}

const config2 = createConfig({ mode: 'development' });
// 타입: { mode: 'development' }

// 실전 활용: 타입 안전한 라우트 정의
const routes = createConfig([
  { path: '/home', name: 'Home' },
  { path: '/about', name: 'About' },
] as const);

type RoutePath = typeof routes[number]['path'];
// 타입: '/home' | '/about'

3. 향상된 열거형 (Enum)

enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}

// TypeScript 5.0+: 모든 열거형 값을 타입으로 가져오기
type LogLevelValue = `${LogLevel}`;
// 타입: "0" | "1" | "2" | "3"

// 실전 활용
function log(level: LogLevel, message: string) {
  if (level <= LogLevel.WARN) {
    console.error(`[${LogLevel[level]}]`, message);
  }
}

고급 타입 시스템

1. 템플릿 리터럴 타입

문자열 리터럴을 조합하여 타입을 생성합니다:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';

// 조합 타입 생성
type APIRoute = `${HTTPMethod} ${Endpoint}`;
// 타입: "GET /users" | "GET /posts" | ... (16개 조합)

// 실전 활용: 타입 안전한 이벤트 핸들러
type EventName = 'click' | 'focus' | 'blur';
type EventHandler<T extends EventName> = `on${Capitalize<T>}`;

function addEventListener<T extends EventName>(
  event: T,
  handler: (e: Event) => void
): EventHandler<T> {
  return `on${event.charAt(0).toUpperCase() + event.slice(1)}` as EventHandler<T>;
}

const handler = addEventListener('click', (e) => {});
// 타입: "onClick"

2. 조건부 타입과 타입 추론

// 배열 요소 타입 추출
type ElementType<T> = T extends (infer U)[] ? U : never;

type StringArray = ElementType<string[]>;  // string
type NumberArray = ElementType<number[]>;  // number

// 함수 반환 타입 추출
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: 'John' };
}

type User = ReturnType<typeof getUser>;
// 타입: { id: number; name: string; }

// Promise 언래핑
type Awaited<T> = T extends Promise<infer U> ? U : T;

async function fetchData() {
  return { data: 'hello' };
}

type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// 타입: { data: string; }

3. 매핑된 타입 (Mapped Types)

// 모든 속성을 optional로 만들기
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 모든 속성을 readonly로 만들기
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 실전 활용: API 응답 타입 생성
type ApiResponse<T> = {
  [K in keyof T]: {
    data: T[K];
    loading: boolean;
    error: Error | null;
  };
};

interface UserData {
  profile: { name: string; email: string };
  posts: Array<{ title: string }>;
}

type UserApiState = ApiResponse<UserData>;
/* 타입:
{
  profile: { data: { name: string; email: string }, loading: boolean, error: Error | null },
  posts: { data: Array<{ title: string }>, loading: boolean, error: Error | null }
}
*/

4. 재귀 타입

TypeScript 4.1+에서 개선된 재귀 타입을 활용합니다:

// JSON 타입 정의
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const config: JSONValue = {
  name: 'App',
  version: 1,
  features: ['auth', 'dashboard'],
  settings: {
    theme: 'dark',
    notifications: true,
  },
};

// 중첩 객체 경로 타입
type PathsToStringProps<T> = T extends string
  ? []
  : {
      [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
    }[Extract<keyof T, string>];

type ObjectPath<T> = PathsToStringProps<T> extends infer P
  ? P extends []
    ? never
    : P extends [infer First, ...infer Rest]
    ? Rest extends []
      ? First
      : `${First & string}.${ObjectPath<{ [K in Rest[0] & string]: any }>}`
    : never
  : never;

interface User {
  profile: {
    name: string;
    address: {
      city: string;
    };
  };
}

type UserPaths = ObjectPath<User>;
// 타입: "profile" | "profile.name" | "profile.address" | "profile.address.city"

유틸리티 타입 활용

interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

// Pick: 특정 속성만 선택
type TodoPreview = Pick<Todo, 'id' | 'title'>;

// Omit: 특정 속성 제외
type TodoFormData = Omit<Todo, 'id' | 'createdAt'>;

// Record: 키-값 타입 정의
type TodoMap = Record<number, Todo>;

// ReturnType: 함수 반환 타입 추출
const createTodo = () => ({
  id: 1,
  title: 'New Todo',
  completed: false,
});

type NewTodo = ReturnType<typeof createTodo>;

활용 팁

  • const 타입 매개변수를 사용하여 리터럴 타입을 보존하세요.
  • 템플릿 리터럴 타입으로 타입 안전한 문자열 조합을 만드세요.
  • 조건부 타입과 타입 추론을 활용하여 유연한 타입을 정의하세요.
  • 유틸리티 타입을 적극 활용하여 코드 중복을 줄이세요.
  • strictNullChecks와 noUncheckedIndexedAccess 옵션을 활성화하여 타입 안전성을 높이세요.

마무리

TypeScript 5.x는 강력한 타입 시스템을 통해 대규모 애플리케이션의 안정성과 유지보수성을 크게 향상시킵니다. 고급 타입 기능을 적절히 활용하면 런타임 에러를 컴파일 타임에 미리 잡아낼 수 있습니다.