제네릭에서의 extends: 제약 vs 조건부

dongEon·2025년 8월 27일
0
post-thumbnail

TypeScript에서 extends는 두 가지 문맥에서 사용됩니다.
1) 제네릭 제약(Constraint)
2) 조건부 타입(Conditional Type)
겉보기엔 똑같아 보여도, 의미와 쓰임새는 전혀 다릅니다. 이번 글에서는 이 둘을 확실히 구분해보고, 마지막에 실전 예제까지 살펴봅니다.


1. 제네릭 제약 (Constraint)

정의

제네릭 파라미터 T가 어떤 타입 U의 부분집합이어야 함을 선언하는 것.
형태: <T extends U>
→ “T는 반드시 U에 호환 가능한 타입이어야 한다.”

예시

function lengthOf<T extends { length: number }>(arg: T): number {
  return arg.length;
}

lengthOf("hello");        // string → length 있음 → OK
lengthOf([1, 2, 3]);      // number[] → length 있음 → OK
lengthOf({ length: 10 }); // 객체도 OK
// lengthOf(123);         // ❌ number에는 length 없음

👉 T extends { length: number } 제약이 걸렸기 때문에, length 속성이 없는 타입은 아예 함수 호출이 불가능합니다.


2. 조건부 타입 (Conditional Type)

정의

조건문처럼 타입을 분기합니다.
형태: T extends U ? X : Y
→ “T가 U에 할당 가능하면 X, 아니면 Y”

예시

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

제네릭과 함께

type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>; // string
type B = ElementType<number>;   // number

👉 infer 키워드를 함께 써서, 조건이 맞으면 내부 타입을 변수처럼 추출해내는 것도 가능합니다.


3. 제약과 조건부를 함께 쓰는 경우

실무에서 자주 쓰는 패턴은 두 가지가 결합된 형태입니다.

function getProp<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
  • T extends object → T는 반드시 객체여야 함
  • K extends keyof T → K는 반드시 T의 키 중 하나여야 함
  • T[K] → K가 유효 키라는 것이 보장되므로 안전하게 값의 타입을 추출

4. 차이 정리

문맥문법의미예시
제약(Constraint)<T extends U>제네릭 선언 시 제한. T는 반드시 U 안에 있어야 한다.<T extends object>
조건부 타입(Conditional Type)T extends U ? X : Y타입 계산 시 분기. T가 U에 할당 가능하면 X, 아니면 Y.T extends string ? "ok" : "no"

5. 실무 활용 패턴

  • 제약으로 안전성 확보

      function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
        return obj[key];
      }

    → K가 반드시 T의 키라는 제약이 있으므로 안전합니다.

  • 조건부 타입으로 타입 변환

      type NonNullable<T> = T extends null | undefined ? never : T;
    
      type A = NonNullable<string | null>; // string
      type B = NonNullable<number | undefined>; // number

Q&A 정리

Q1. 제네릭 제약에서의 extends는 어떤 느낌인가요?

  • “이 타입은 반드시 저 타입의 하위 집합이어야 한다”라는 규칙을 미리 거는 것.
  • 런타임에서 if 체크를 하는 게 아니라, 애초에 호출/사용 자체가 금지됩니다.

Q2. 조건부 타입에서의 extends는 어떻게 다른가요?

  • 타입 레벨의 삼항 연산자 같은 것.
  • 타입 T를 실제로 넣으면 결과가 X 또는 Y로 “계산”됩니다.
  • 즉, 타입을 실행해서 결과를 얻는 로직이라고 이해하면 편해요.

6. 실전 예시: RequestOf / ResponseOf

Day4 문제인 api-endpoints를 예로 들어보겠습니다.

export type ApiMap = {
  "/login": {
    req: { id: string; pw: string };
    res: { token: string };
  };
  "/me": {
    res: { id: string; name: string };
  };
  "/items": {
    req: { page: number };
    res: { items: Array<{ id: string; name: string }> };
  };
};

export type RequestOf<M, K extends keyof M> =
  "req" extends keyof M[K] ? M[K]["req"] : never;

export type ResponseOf<M, K extends keyof M> =
  M[K]["res"];

여기서 쓰이는 extends 두 가지 문맥:

  1. 제약

    • <K extends keyof M>
    • K는 반드시 M 객체의 키 중 하나여야 한다.
    • 즉, "/login", "/me", "/items" 중 하나여야 함.
  2. 조건부 타입

    • "req" extends keyof M[K] ? ... : ...
    • M[K]에 "req" 키가 존재하면 M[K]["req"],
      아니면 never.
    • 즉, 조건부 분기로 req가 없는 엔드포인트("/me")에서는 never 처리.

👉 이 조합 덕분에, "/login" 같은 엔드포인트에서는 요청 타입이 나오고, "/me" 같은 요청 없는 엔드포인트에서는 자동으로 never가 됩니다.


✅ 결론

  • 제약(Constraint):제약 조건입니다. 규칙이라고 생각하세요. 제공하는 유형 K는 반드시 M의 키여야 한다. 이는 허용되는 유형을 제한하는 것입니다.
  • 조건부 타입(Conditional Type): 타입 레벨에서 if/else 분기
  • 실무에서는 두 가지가 함께 쓰이며, RequestOf 같은 유틸 타입에서 “키 제약 + 조건부 분기” 패턴이 핵심적으로 활용됩니다.

profile
개발 중에 마주한 문제와 해결 과정, 새롭게 배운 지식, 그리고 알고리즘 문제 해결에 대한 다양한 인사이트를 공유하는 기술 블로그입니다

0개의 댓글