본문으로 건너뛰기

안동민 개발노트

본문 시작

제네릭 제약 조건

extends로 제네릭 타입이 가져야 할 속성·클래스·생성자 관계를 제한해 구현 내부에서 허용된 연산을 안전하게 사용합니다.

앞서 3장 4절 제네릭 함수에서 제네릭의 기본적인 개념을 다루면서, 타입 변수 T가 모든 타입을 허용하는 것이 아니라 특정 조건을 만족하는 타입만 허용하도록 제한할 수 있다고 간략하게 설명했습니다.

이것이 바로 제네릭 제약 조건(Generic Constraints)입니다.

제네릭 제약 조건은 extends 키워드를 사용하여 정의하며, 특정 타입 변수가 특정 인터페이스, 클래스 또는 다른 타입에 할당 가능해야 한다는 제약을 설정합니다.

이를 통해 제네릭 함수나 클래스 내부에서 타입 변수에 대해 더 구체적인 작업을 수행할 수 있도록 타입 안정성을 확보할 수 있습니다.

제네릭 제약의 구성 요소

제네릭은 여러 타입을 받을 수 있지만, 함수 내부에서 특정 속성을 써야 한다면 `extends`로 최소 조건을 선언해야 합니다.

  1. 타입 변수

    `T`는 호출 시점에 결정되는 타입이므로 아무 조건이 없으면 내부 속성 접근이 제한됩니다.

  2. extends 조건

    `T extends Lengthwise`는 length 속성을 가진 값만 통과시키는 컴파일 규칙입니다.

  3. keyof 제약

    `K extends keyof T`를 쓰면 객체에 실제 존재하는 키만 안전하게 받을 수 있습니다.

  4. 반환 타입 유지

    제약은 타입을 좁히는 것이 아니라 필요한 조건만 더해 원래 타입 정보를 보존합니다.


extends를 사용한 제약 조건의 기본

제네릭 제약은 넓게 받고 필요한 조건만 증명한다

T를 완전히 고정하지 않고 extends로 최소 조건을 요구하면 재사용성과 안전성을 동시에 지킬 수 있다.

  1. Input T 호출자

    타입 결정

  2. extends 필요 속성만 요구
  3. Narrowing 조건문

    더 좁힘

  4. Return 입력 타입 관계

    유지

점검기준
좋은 제약T extends { id: string }
과한 제약필요 없는 속성까지 요구
핵심제네릭은 열어두되 내부에서 필요한 만큼만 잠근다

제네릭 제약 조건은 타입 변수 뒤에 extends 키워드와 함께 제약 타입을 명시하여 선언합니다.

// T extends ConstraintType
  • T: 제약을 걸 타입 변수.
  • extends: TConstraintType에 할당 가능해야 함을 의미하는 키워드.
  • ConstraintType: T가 만족해야 할 조건 (인터페이스, 클래스, 원시 타입 등).

가장 기본적인 예시로, 특정 속성을 가진 객체만 받을 수 있도록 제약을 걸어보겠습니다.

interface Lengthwise {
  length: number; // length 속성이 number 타입이어야 한다고 정의
}

// getLength 함수는 T 타입의 인자를 받지만,
// T는 반드시 Lengthwise 인터페이스를 확장(만족)해야 합니다.
function getLength<T extends Lengthwise>(arg: T): number {
  return arg.length; // 이제 arg.length에 안전하게 접근할 수 있습니다.
}

// Lengthwise 인터페이스를 만족하는 타입들:
console.log(getLength("hello world"));     // string은 length 속성을 가짐: 11
console.log(getLength([1, 2, 3, 4, 5]));   // 배열은 length 속성을 가짐: 5
console.log(getLength({ length: 10, value: "test" })); // length 속성을 가진 객체 리터럴: 10

// Lengthwise 인터페이스를 만족하지 않는 타입:
// getLength(123);      // Error: 'number' 형식은 'Lengthwise' 형식에 할당할 수 없습니다.
// getLength(true);     // Error: 'boolean' 형식은 'Lengthwise' 형식에 할당할 수 없습니다.
// getLength({});       // Error: '{}' 형식에 'length' 속성이 없습니다.

getLength 함수에서 T extends Lengthwise 제약 조건을 추가함으로써, 타입스크립트 컴파일러는 arg가 반드시 length: number 속성을 가질 것임을 알게 됩니다.

따라서 함수 내부에서 arg.length에 접근하는 것이 타입 오류 없이 가능해집니다.


여러 타입 변수에 제약 조건 적용

제네릭 함수나 클래스가 여러 타입 변수를 가질 때, 각각의 타입 변수에 독립적인 제약 조건을 적용할 수 있습니다.

// K는 T의 키 타입에 할당 가능해야 함을 제약합니다.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

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

const person: Person = { name: "철수", age: 30 };

let personName = getProperty(person, "name"); // type personName = string
let personAge = getProperty(person, "age");   // type personAge = number

// getProperty(person, "address"); // Error: 'address' 형식은 'keyof Person' 형식에 할당할 수 없습니다.

여기서 K extends keyof TKT의 모든 속성 이름으로 구성된 유니온 타입('name' | 'age') 중 하나여야 한다는 제약을 설정합니다.

이를 통해 getProperty 함수는 전달된 obj의 유효한 키만 받도록 강제하여 타입 안전성을 높입니다.

아래 다이어그램은 제네릭 제약을 어떤 상황에서 어떤 형태로 고르면 좋은지 비교한 것입니다.

키 제약은 객체 타입과 키 타입을 한 묶음으로 묶는다

K extends keyof T 는 아무 문자열이나 받지 않고, 실제 객체 타입 T 에 존재하는 속성 이름만 고르게 만드는 장치입니다.

  1. getProperty(person, "name")

    string 으로 추론됩니다.

  2. getProperty(person, "age")

    number 로 추론됩니다.

  3. 존재하지 않

    키는 문자열이어도 keyof T 밖이라 실패합니다.

순서역할타입 신호검사 효과
01객체 입력obj: T객체의 구체 속성 목록을 타입 변수에 담아 호출부 정보를 잃지 않습니다.
02키 입력key: K키도 별도 타입 변수로 받아 선택한 속성 이름을 추론하게 합니다.
03키 제약K extends keyof T객체 타입에 없는 문자열을 컴파일 단계에서 바로 거절합니다.
04값 접근obj[key]접근 가능한 키임이 보장되므로 색인 접근이 안전해집니다.
05결과 타입T[K]선택한 키에 맞춰 문자열 또는 숫자 결과가 따라옵니다.

제약 조건으로서의 class 타입

제네릭 타입 변수에 특정 클래스를 제약 조건으로 사용하여, 해당 클래스의 인스턴스 또는 그 자식 클래스의 인스턴스만 허용하도록 할 수 있습니다.

이는 특히 팩토리 함수(Factory function)를 만들 때 유용합니다.

class Animal {
  constructor(public name: string) {}
  eat() { console.log(`${this.name}가 먹습니다.`); }
}

class Dog extends Animal {
  constructor(name: string) { super(name); }
  bark() { console.log(`${this.name}가 멍멍 짖습니다.`); }
}

class Cat extends Animal {
  constructor(name: string) { super(name); }
  purr() { console.log(`${this.name}가 야옹거립니다.`); }
}

// createInstance 함수는 T extends Animal 클래스의 생성자를 받습니다.
// new (...args: any[]) => T : T 타입의 인스턴스를 생성하는 생성자 시그니처
function createInstance<T extends Animal>(Constructor: new (...args: any[]) => T, name: string): T {
  return new Constructor(name);
}

const myDog = createInstance(Dog, "바둑이"); // myDog의 타입은 Dog
myDog.bark(); // 바둑이(이)가 멍멍 짖습니다.
myDog.eat();  // 바둑이(이)가 먹습니다.

const myCat = createInstance(Cat, "나비");   // myCat의 타입은 Cat
myCat.purr(); // 나비(이)가 야옹거립니다.

// const myAnimal = createInstance(Animal, "복슬이"); // myAnimal의 타입은 Animal
// myAnimal.bark(); // Error: 'Animal' 형식에 'bark' 속성이 없습니다.

// createInstance(String, "abc"); // Error: 'String' 형식은 'new (...args: any[]) => Animal' 형식에 할당할 수 없습니다.

여기서 T extends AnimalTAnimal 클래스 또는 Animal을 상속받는 클래스 타입임을 의미합니다.

Constructor: new (...args: any[]) => TConstructor 매개변수가 T 타입 인스턴스를 생성할 수 있는 생성자 함수임을 나타냅니다.

덕분에 createInstance 함수는 다양한 Animal 서브클래스 인스턴스를 타입 안전하게 생성할 수 있습니다.


typeof를 제약 조건으로 사용

특정 원시 타입이나 리터럴 타입에 대해 제약을 걸 수도 있습니다.

function printLengthIfString<T extends string | string[]>(data: T): void {
    if (typeof data === 'string') {
        console.log(`문자열 길이: ${data.length}`);
    } else {
        console.log(`배열 길이: ${data.length}`);
    }
}

printLengthIfString("hello"); // 문자열 길이: 5
printLengthIfString(["a", "b", "c"]); // 배열 길이: 3

// printLengthIfString(123); // Error: 'number' 형식은 'string | string[]' 형식에 할당할 수 없습니다.

이 예시에서는 T extends string | string[]를 사용하여 datastring 또는 string 배열이어야 함을 강제합니다.


제약 조건과 타입 추론

제네릭 제약 조건은 타입 추론에도 영향을 미칩니다.

타입스크립트는 제약 조건을 기반으로 타입 변수의 최소한의 형태를 추론할 수 있습니다.

function mergeObjects<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = mergeObjects({ a: 1, b: "hello" }, { c: true, d: [1, 2] });
// merged의 타입은 { a: number; b: string; } & { c: boolean; d: number[]; }
// 이는 { a: number; b: string; c: boolean; d: number[]; } 와 동일합니다.

// mergeObjects(1, {}); // Error: 'number' 형식은 'object' 형식에 할당할 수 없습니다.

T extends objectU extends object 제약 조건 덕분에, 함수 내부에서 obj1obj2가 객체 타입이며 스프레드 문법(...)을 안전하게 사용할 수 있음을 보장받습니다.

또한 반환 타입 T & U는 두 객체 타입의 모든 속성을 포함하는 인터섹션 타입으로 정확하게 추론됩니다.


제네릭 제약 조건은 타입스크립트에서 제네릭의 유연성과 타입 안전성을 동시에 확보하는 핵심적인 메커니즘입니다.

이를 통해 우리는 모든 타입을 무분별하게 허용하는 대신, 필요한 조건을 명시하여 더 강력하고 예측 가능한 제네릭 컴포넌트들을 설계할 수 있습니다.

extends 키워드를 사용하여 인터페이스, 클래스, 또는 다른 타입을 제약 조건으로 활용하는 방법을 숙지하는 것이 중요합니다.

제약 조건은 타입을 좁히는 장치이면서 함수 내부에서 무엇을 안전하게 사용할 수 있는지 알려주는 설계 도구입니다.

아래 표는 제네릭에 제약을 걸어야 할 때 어떤 기준으로 형태를 고르면 좋은지 정리합니다.

제네릭 제약 설계 판단 기준

제약 조건은 타입을 줄이는 문법이 아니라 함수 내부에서 사용할 수 있는 능력을 보장하는 계약입니다.

  1. 속성이 필요하다

    property T extends { length: number } 처럼 함수가 읽는 속성만 최소로 요구합니다.

  2. 유효한 키만 받는다

    key K extends keyof T 로 존재하지 않는 속성 접근을 호출부에서 막습니다.

  3. 스프레드를 쓴다

    object extends object 로 원시값을 제외하고 병합 가능한 입력만 허용합니다.

  4. 생성자가 필요하다

    new 팩토리 함수는 인스턴스 타입과 생성자 시그니처를 함께 제약합니다.

제네릭 제약은 입력을 제한하는 데서 끝나지 않습니다.

함수 내부에서 필요한 멤버를 안전하게 쓰면서도 호출자가 넣은 구체 타입이 반환까지 보존되는지 함께 확인해야 합니다.

제약과 결과 보존

T extends ... 는 입력을 제한하려는 문법이면서, 함수 내부에서 안전하게 쓸 수 있는 속성과 반환 타입의 정밀도를 함께 결정합니다.

  1. 함수가 실제로 쓰는 멤버 확인

    내부에서 length , id , 생성자 호출처럼 필요한 접근을 먼저 적습니다.

  2. 가장 작은 제약 만들기

    필요한 속성만 가진 인터페이스나 keyof T 로 허용 범위를 좁힙니다.

  3. 입력의 구체 타입 유지

    제약 타입 대신 T 를 유지해 호출 결과를 보존합니다.

  4. 오류 메시지로 제약 검증

    거절된 값과 제약 범위를 함께 조정합니다.

  5. 제약의 모양

    extends Lengthwise K extends keyof T new (...) => T 함수가 실제로 사용하는 능력만 요구하고, 호출자가 넣은 타입 정보는 반환까지 이어집니다.

  6. 주의할 냄새

    extends object Record<string, any> 너무 넓은 제약은 함수 내부의 접근을 충분히 설명하지 못합니다. 필요한 키와 값의 형태를 더 구체화합니다.


다음 다이어그램은 제네릭 제약 조건을 필요한 능력만 요구하는 관점에서 정리한 표입니다.

제네릭 제약은 자유로운 타입에 필요한 조건만 더합니다

extends, keyof, class 타입, typeof를 활용하면 제네릭의 재사용성은 유지하면서 함수가 실제로 쓰는 능력만 요구할 수 있습니다.

  1. 최소 구조 요구

    extends T가 특정 속성이나 메서드를 가진 값이어야 할 때 구조 제약을 둡니다. T extends Lengthwise

  2. 키 관계 고정

    keyof 객체와 키를 함께 받을 때 실제 키만 허용하도록 연결합니다. K extends keyof T

  3. 생성 가능성

    class 타입 new로 만들 수 있는 타입을 받아 인스턴스 생성 패턴을 안전하게 표현합니다. new () => T

  4. 구체 타입 유지

    추론 보존 제약을 걸어도 호출 시점의 구체 타입 정보는 가능한 한 유지합니다. T

필요한 만큼만 제약하는 법

모든 타입을 허용하면 내부에서 할 수 있는 일이 줄어들기 때문에 extends로 최소한의 구조나 관계를 요구합니다.

  1. 최소 구조 요구

    extends length나 id처럼 내부에서 사용할 속성이 있다면 그 구조를 만족하는 타입으로 제한합니다. T extends

  2. 관계 설정

    여러 변수 두 타입 변수 사이의 포함 관계를 만들어 키와 객체처럼 연결된 값을 안전하게 다룹니다. K extends keyof T

  3. 생성자 형태 요구

    class 제약 인스턴스를 만들거나 클래스 타입을 받아야 할 때 생성자 시그니처를 제약으로 둡니다. new () => T

  4. 필요한 만큼만 제한

    추론 유지 제약을 너무 넓거나 좁게 잡지 않으면 호출 시점의 구체 타입 정보가 잘 보존됩니다. inference

아래 다이어그램은 제네릭 제약 조건을 필요한 능력만 요구하는 방식으로 정리합니다.

제네릭 제약 조건 설계

제네릭 제약은 타입 변수를 좁혀 함수 내부에서 안전하게 사용할 속성과 메서드를 보장합니다.

  1. extends 제약
    필요 조건 선언

    extends 제약 T가 특정 구조에 할당 가능해야 함을 타입 변수 선언에서 명시합니다. T extends Constraint

  2. 여러 변수
    관계 제약 표현

    여러 변수 객체 타입 T와 그 키 K처럼 타입 변수 사이의 의존 관계를 선언합니다. K extends keyof T

  3. class 타입
    생성자와 인스턴스

    class 타입 클래스 자체를 값으로 받을 때 new 가능한 타입을 제약으로 표현합니다. new () => T

  4. typeof 활용
    값에서 타입 얻기

    typeof 활용 이미 존재하는 값의 타입을 추출해 제약이나 매개변수 타입에 재사용합니다. typeof config