icon안동민 개발노트

제네릭 제약 조건


 제네릭 제약 조건은 타입스크립트에서 제네릭 타입의 범위를 제한하는 강력한 기능입니다.

 이를 통해 더 정확한 타입 체크와 향상된 타입 안정성을 얻을 수 있습니다.

개념과 필요성

 제네릭 제약 조건은 제네릭 타입이 특정 조건을 만족해야 함을 명시합니다.

 이는 제네릭의 유연성을 유지하면서도 타입의 특정 속성이나 메서드에 안전하게 접근할 수 있게 해줍니다.

function logLength<T extends { length: number }>(arg: T): void {
    console.log(arg.length);  // 안전하게 .length 접근 가능
}
 
logLength("Hello");  // 가능
logLength([1, 2, 3]);  // 가능
// logLength(123);  // 오류: number 타입에는 'length' 속성이 없음

 이 예시에서 T extends { length: number }는 T가 length 속성을 가져야 한다는 제약 조건을 부여합니다.

인터페이스를 사용한 복잡한 제약 조건

 더 복잡한 제약 조건은 인터페이스나 타입 별칭을 사용하여 정의할 수 있습니다.

interface Sizeable {
    size: number;
    resize(newSize: number): void;
}
 
function resizeItem<T extends Sizeable>(item: T, newSize: number): T {
    item.resize(newSize);
    return item;
}
 
class Box implements Sizeable {
    constructor(public size: number) {}
    resize(newSize: number): void {
        this.size = newSize;
    }
}
 
const box = new Box(10);
resizeItem(box, 20);  // 정상 작동

키 제약 조건 keyof

 keyof 키워드를 사용하면 객체의 속성에 안전하게 접근할 수 있습니다.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
 
const person = { name: "Alice", age: 30 };
const name = getProperty(person, "name");  // 타입: string
const age = getProperty(person, "age");    // 타입: number
// getProperty(person, "location");  // 오류: "location"은 person의 키가 아님

조건부 타입과 제네릭 제약 조건의 결합

 조건부 타입과 제네릭 제약 조건을 결합하면 더욱 유연한 타입 설계가 가능합니다.

type ArrayIfString<T> = T extends string ? T[] : T;
 
function processInput<T extends string | number>(input: T): ArrayIfString<T> {
    if (typeof input === "string") {
        return input.split("") as ArrayIfString<T>;
    }
    return input as ArrayIfString<T>;
}
 
const result1 = processInput("hello");  // 타입: string[]
const result2 = processInput(123);      // 타입: number

기본 타입 지정

 제네릭 제약 조건에서 기본 타입을 지정할 수 있습니다.

interface DefaultDict<T = string> {
    [key: string]: T;
}
 
const dict1: DefaultDict = { key: "value" };  // T는 기본값 string
const dict2: DefaultDict<number> = { key: 123 };  // T는 명시적으로 number

다중 타입 매개변수 제약 조건

 여러 타입 매개변수에 대해 제약 조건을 설정할 수 있습니다.

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}
 
const merged = merge({ name: "Alice" }, { age: 30 });
// 타입: { name: string } & { age: number }

팩토리 함수와 제네릭 제약 조건

 제네릭 제약 조건은 팩토리 함수에서 유용하게 사용될 수 있습니다.

interface Constructable<T> {
    new (): T;
}
 
function createInstance<T>(ctor: Constructable<T>): T {
    return new ctor();
}
 
class MyClass {
    value: number = 42;
}
 
const instance = createInstance(MyClass);
console.log(instance.value);  // 42

흔한 오류와 해결 방법

  1. 제약 조건과 실제 타입의 불일치
function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}
 
// getLength(42);  // 오류: number 타입은 { length: number }를 만족하지 않음
 
// 해결:
getLength([1, 2, 3]);  // 배열은 length 속성을 가짐
getLength("hello");    // 문자열도 length 속성을 가짐
  1. 과도하게 제한적인 제약 조건
// 과도하게 제한적:
function printName<T extends { name: string; age: number }>(obj: T): void {
    console.log(obj.name);
}
 
// 더 유연한 방식:
function printName<T extends { name: string }>(obj: T): void {
    console.log(obj.name);
}

Best Practices와 주의사항

  1. 최소한의 제약 조건 사용 : 필요한 속성이나 메서드만 제약 조건으로 지정하세요.
  2. 인터페이스 활용 : 복잡한 제약 조건은 인터페이스로 분리하여 재사용성을 높이세요.
  3. 명확한 명명 : 제네릭 타입 매개변수와 제약 조건에 의미 있는 이름을 사용하세요.
  4. 기본 타입 고려 : 적절한 경우 기본 타입을 제공하여 사용 편의성을 높이세요.
  5. 다중 제약 조건 주의 : 여러 제약 조건을 사용할 때는 그 관계를 신중히 고려하세요.
  6. 타입 추론 활용 : 가능한 경우 타입스크립트의 타입 추론을 활용하여 코드를 간결하게 유지하세요.
  7. 문서화 : 복잡한 제약 조건은 주석으로 설명하여 다른 개발자의 이해를 돕습니다.
  8. 테스트 작성 : 제네릭 제약 조건을 사용한 함수나 클래스에 대한 단위 테스트를 작성하세요.
  9. 과도한 사용 주의 : 제네릭 제약 조건이 코드를 복잡하게 만들지 않도록 주의하세요.
  10. 실제 사용 사례 기반 : 실제 문제 해결을 위해 제네릭 제약 조건을 사용하고, 불필요한 추상화는 피하세요.

 제네릭 제약 조건은 타입스크립트에서 타입 안정성과 코드의 재사용성을 높이는 강력한 도구입니다.

 이를 통해 제네릭의 유연성을 유지하면서도 특정 조건을 만족하는 타입만을 허용할 수 있습니다.