고급 TypeScript 패턴: 스케일링 가능한 타입 안전 코드

발행: (2026년 1월 7일 오전 03:34 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

Advanced TypeScript Patterns: Type‑Safe Code That Scales의 커버 이미지

Sepehr Mohseni

제네릭 제약과 추론

제약이 있는 기본 제네릭

// Constrain a generic to objects that have an `id` property
interface HasId {
  id: string | number;
}

/**
 * Find an element by its `id`.
 * @param items - Array of items that extend `HasId`
 * @param id    - The id to look for (must be the same type as the item's id)
 */
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
  return items.find(item => item.id === id);
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com' },
];

const user = findById(users, 1); // Type: User | undefined

제네릭 팩토리 함수

/**
 * Simple store implementation with type‑safe state handling.
 * @param initialState - The initial state of the store; its type is inferred.
 */
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<(s: T) => void>();

  return {
    /** Returns the current state */
    getState: () => state,

    /**
     * Merges a partial update into the current state.
     * @param newState - Partial object containing the fields to update.
     */
    setState: (newState: Partial<T>) => {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener(state));
    },

    /**
     * Subscribes a listener that will be called on every state change.
     * @param listener - Function receiving the new state.
     * @returns A function to unsubscribe the listener.
     */
    subscribe: (listener: (state: T) => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const userStore = createStore({
  user: null as User | null,
  isLoading: false,
  error: null as string | null,
});

userStore.setState({ isLoading: true }); // ✅ Type‑safe

Tip: TypeScript는 사용으로부터 복잡한 타입을 추론할 수 있습니다. 내부 구현에서는 컴파일러에게 무거운 작업을 맡기되, 공개 API에서는 항상 명시적인 제네릭 제약(e.g., T extends HasId)을 추가하여 명확하고 안전하게 유지하세요.

Source:

조건부 타입

기본 조건부 타입

// Generic conditional type that maps a key to a response type
type ApiResponse<T> = T extends 'user'
  ? User
  : T extends 'product'
    ? Product
    : never;

// Example payload types
interface User {
  id: number;
  name: string;
}
interface Product {
  id: number;
  price: number;
}

// Usage
type UserResponse    = ApiResponse<'user'>;    // → User
type ProductResponse = ApiResponse<'product'>; // → Product

실용적인 예 – Promise 타입 풀기

// Simplified version of the built‑in `Awaited` type
type Unwrap<T> = T extends Promise<infer R> ? Unwrap<R> : T;

// Example
type Result = Unwrap<Promise<Promise<string>>>; // → string

분배형 조건부 타입

조건부 타입은 유니온 타입에 자동으로 분배됩니다. 이를 통해 유니온의 각 멤버를 개별적으로 필터링하거나 변환하는 작업이 편리해집니다.

null / undefined 제거

// Remove `null` and `undefined` from a type
type NonNullable<T> = T extends null | undefined ? never : T;

// Example
type Example = NonNullable<string | null | undefined>; // → string

유니온에서 특정 멤버 추출

// Keep only the `string` members of a union
type ExtractStrings<T> = T extends string ? T : never;

type Mixed       = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // → string | 'hello' | 'world'

Note: ExtractStrings<T> 조건부 타입이 Mixed의 각 멤버에 개별적으로 적용되기 때문에, extends string 검사를 통과한 멤버들의 유니온이 결과로 반환됩니다. 이 분배형 동작이 TypeScript에서 강력한 타입‑레벨 필터링을 가능하게 합니다.

타입 추출을 위한 infer 키워드

infer 키워드는 조건부 타입 내부에서 타입을 캡처하여 다른 곳에서 재사용할 수 있게 해줍니다. 아래는 타입에서 정보를 추출하는 가장 흔한 패턴들입니다.

// ------------------------------------------------------------
// 1️⃣ 함수의 매개변수 타입 추출
// ------------------------------------------------------------
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

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

// ------------------------------------------------------------
// 3️⃣ 배열(또는 튜플)의 요소 타입 추출
// ------------------------------------------------------------
type ArrayElement<T> = T extends (infer E)[] ? E : never;

// ------------------------------------------------------------
// 4️⃣ React 컴포넌트의 props 타입 추출
// ------------------------------------------------------------
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

📚 실용 예제

// 간단한 async 함수
async function fetchUser(id: number): Promise<User> {
  /* … */
}

// 위에서 정의한 헬퍼 사용
type FetchUserParams = Parameters<typeof fetchUser>; // => [number]
type FetchUserReturn = Return<typeof fetchUser>;     // => Promise<User>

헬퍼가 하는 일

헬퍼입력추출된 타입
Parameters<T>(...args: infer P) => anyP – 함수 매개변수들의 튜플
Return<T>(...args: any[]) => infer RR – 함수의 반환 타입
ArrayElement<T>(infer E)[]E – 배열 각 요소의 타입
ComponentProps<T>React.ComponentType<infer P>P – 컴포넌트의 props 타입

이 유틸리티들은 TypeScript에서 고차 함수, 타입이 지정된 래퍼, 혹은 제네릭 컴포넌트 라이브러리와 같은 고급 타입‑레벨 패턴을 구현하기 위한 기본 블록입니다.

Source:

매핑된 타입

객체 타입 변환

// Make all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Make all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Pick specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

고급 매핑된 타입

// Create getters for all properties
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

/* Example */
type PersonGetters = Getters<Person>;
/* Result:
{
  getName: () => string;
  getAge: () => number;
}
*/

// Filter properties by their value type
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
}

/* Example */
type NumberProps = FilterByType<Mixed, number>;
/* Result:
{
  id: number;
  count: number;
}
*/

Note: 템플릿 리터럴 타입매핑된 타입을 결합하면 강력한 타입 변환을 만들 수 있습니다—강력히 타입이 지정된 API 클라이언트, 폼 빌더 및 기타 메타프로그래밍 시나리오를 생성하는 데 완벽합니다.

타입 가드와 좁히기

사용자 정의 타입 가드

// Type predicate function
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).email === "string"
  );
}

// Usage
function processData(data: unknown) {
  if (isUser(data)) {
    // TypeScript knows `data` is `User` here
    console.log(`User ${data.name} (${data.id})`);
  } else {
    console.warn("Not a user");
  }
}

타입 가드가 있는 차별화된 유니온

interface SuccessResponse {
  status: 'success';
  data: User;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // TypeScript narrows to SuccessResponse
    console.log(response.data.name);
  } else {
    // TypeScript narrows to ErrorResponse
    console.log(response.message);
  }
}

Assertion Functions

/**
 * 제공된 값이 `User`인지 확인합니다.
 * 검사에 실패하면 오류를 발생시켜 TypeScript가 타입을 좁히도록 합니다.
 */
function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error('Value is not a User');
  }
}

/** 사용 예시 */
function processUserData(data: unknown) {
  // 단언 후 `data`를 `User`로 좁힘
  assertIsUser(data);
  console.log(data.email); // 이제 `data`는 `User`임이 알려짐
}

/**
 * 값이 `null`도 `undefined`도 아닌지 확인합니다.
 *
 * @param value   확인할 값.
 * @param message 선택적인 사용자 정의 오류 메시지.
 *
 * @throws `value`가 `null`이거나 `undefined`인 경우 오류를 발생시킵니다.
 */
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message ?? 'Value is not defined');
  }
}

템플릿 리터럴 타입

원본 섹션에 제공된 코드 예제가 없습니다.

타입‑안전 API 구축

// ------------------------------------------------------------
// 1️⃣ Event handler types
// ------------------------------------------------------------

// Allowed event names
type EventName = 'click' | 'focus' | 'blur';

// Utility type that builds the handler name (e.g. "onClick")
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;

// Example usage
type ClickHandler = EventHandler<'click'>; // "onClick"
type FocusHandler = EventHandler<'focus'>; // "onFocus"
type BlurHandler  = EventHandler<'blur'>;  // "onBlur"
// ------------------------------------------------------------
// 2️⃣ Route parameters extraction
// ------------------------------------------------------------

// Route patterns
type Route =
  | '/users/:id'
  | '/posts/:postId/comments/:commentId';

/**
 * Recursively extracts all `:param` placeholders from a route string.
 *
 * @template T - The route string to analyse.
 * @returns A union of the extracted parameter names.
 */
type ExtractParams<T extends string> =
  // Match a segment that contains a parameter followed by more segments
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    // Match the final segment that contains a parameter
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      // No more parameters
      : never;

// Example extractions
type UserRouteParams    = ExtractParams<'/users/:id'>; // "id"
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
//   → "postId" | "commentId"

위 스니펫은 다음을 보여줍니다:

  1. 템플릿 리터럴 타입과 Capitalize를 사용해 타입‑안전 이벤트‑핸들러 이름 생성
  2. URL 패턴에서 라우트 파라미터를 추출하여 플레이스홀더 이름의 유니온을 얻기

타입‑안전 CSS 속성

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

interface Spacing {
  margin: CSSValue;
  padding: CSSValue;
}

const spacing: Spacing = {
  margin: '16px',    // Valid
  padding: '1.5rem', // Valid
  // margin: '16',   // Error: not a valid CSSValue
};

실전에서의 유틸리티 타입

(섹션이 유지되었습니다; 구체적인 코드는 제공되지 않았습니다.)

Form 라이브러리 만들기

아래는 작고 타입‑안전한 폼‑구성 도우미입니다. 데이터 모델의 형태를 기반으로 적절한 입력 타입을 추론하고, 라벨, 검증, 필수 플래그를 추가할 수 있습니다.

// ------------------------------------------------------------
// 1️⃣  Generic field‑configuration type
// ------------------------------------------------------------
type FieldConfig<T> = {
  [K in keyof T]: {
    /** Input type inferred from the property type */
    type:
      T[K] extends string
        ? 'text' | 'email' | 'password'
        : T[K] extends number
        ? 'number'
        : T[K] extends boolean
        ? 'checkbox'
        : 'text';

    /** Human‑readable label */
    label: string;

    /** Optional “required” flag */
    required?: boolean;

    /** Optional validator – returns an error string or `undefined` */
    validate?: (value: T[K]) => string | undefined;
  };
};

// ------------------------------------------------------------
// 2️⃣  Example data model
// ------------------------------------------------------------
interface UserForm {
  name: string;
  email: string;
  age: number;
  newsletter: boolean;
}

// ------------------------------------------------------------
// 3️⃣  Concrete configuration for `UserForm`
// ------------------------------------------------------------
const userFormConfig: FieldConfig<UserForm> = {
  name: {
    type: 'text',
    label: 'Full Name',
    required: true,
    validate: value => 
  }
}
Back to Blog

관련 글

더 보기 »

React 컴포넌트에서 TypeScript Generics

소개 제네릭은 React 컴포넌트에서 매일 사용하는 것은 아니지만, 특정 경우에는 유연하고 타입‑안전한 컴포넌트를 작성할 수 있게 해줍니다.