icon안동민 개발노트

제네릭 함수


 제네릭은 타입스크립트의 강력한 기능 중 하나로, 다양한 타입에 대해 재사용 가능한 컴포넌트를 작성할 수 있게 해줍니다.

 이는 코드의 유연성과 재사용성을 높이며, 동시에 타입 안정성을 보장합니다.

제네릭의 개념과 필요성

 제네릭을 사용하면 함수나 클래스가 다양한 타입에 대해 작동할 수 있도록 일반화할 수 있습니다.

 이는 코드 중복을 줄이고 타입 안정성을 유지하면서도 유연한 API를 설계할 수 있게 해줍니다.

기본 문법과 예시

function identity<T>(arg: T): T {
    return arg;
}
 
let output = identity<string>("myString");  // 타입: string
let output2 = identity(42);  // 타입: number (타입 추론)

 여기서 T는 타입 매개변수입니다.

 이 함수는 어떤 타입의 인자든 받아 그대로 반환합니다.

다중 타입 매개변수

function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}
 
let p = pair<string, number>("hello", 42);  // 타입: [string, number]

 이 예시는 두 개의 타입 매개변수를 사용하여 다양한 타입 조합을 표현합니다.

제네릭 제약조건

 extends 키워드를 사용하여 타입 매개변수가 특정 타입이나 인터페이스를 확장해야 한다고 지정할 수 있습니다.

interface Lengthwise {
    length: number;
}
 
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // 이제 .length 속성에 안전하게 접근 가능
    return arg;
}

keyof와 제네릭

 keyof 연산자를 제네릭과 함께 사용하면 객체의 속성에 안전하게 접근할 수 있습니다.

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // 정상
getProperty(x, "m"); // 오류: 인수의 타입 '"m"'은 '"a" | "b" | "c" | "d"' 타입의 매개변수에 할당될 수 없습니다.

기본 타입 지정

 제네릭 타입에 기본값을 지정할 수 있습니다.

function createArray<T = string>(length: number, value: T): T[] {
    return new Array(length).fill(value);
}
 
let arr1 = createArray(3, "a");  // 타입: string[]
let arr2 = createArray(2, 0);    // 타입: number[]

조건부 타입과 제네릭

 조건부 타입을 제네릭과 함께 사용하면 복잡한 타입 로직을 표현할 수 있습니다.

type TypeName<T> = 
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";
 
type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;     // "string"
type T2 = TypeName<true>;    // "boolean"

타입 추론과 명시적 타입 인자

 타입스크립트는 대부분의 경우 제네릭 함수의 타입을 자동으로 추론할 수 있습니다.

 그러나 때로는 명시적으로 타입을 지정해야 할 수도 있습니다.

function merge<T, U>(obj1: T, obj2: U) {
    return { ...obj1, ...obj2 };
}
 
let merged = merge({ name: "John" }, { age: 30 });  // 타입 추론
let merged2 = merge<{ name: string }, { age: number }>({ name: "John" }, { age: 30 });  // 명시적 타입 지정

 명시적 타입 지정은 더 명확한 타입 정보를 제공하지만, 코드가 더 장황해질 수 있습니다.

흔한 오류와 해결 방법

  1. 제약 조건과 관련된 오류
function getLength<T>(arg: T): number {
    return arg.length;  // 오류: 'T' 타입에 'length' 속성이 없습니다.
}
 
// 해결
function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}
  1. 타입 매개변수 이름 충돌
function example<T, T>(a: T, b: T) {}  // 오류: 'T'가 중복 식별자입니다.
 
// 해결
function example<T, U>(a: T, b: U) {}

Best Practices와 주의사항

  1. 명확성을 위해 의미 있는 타입 매개변수 이름 사용 (예 : T보다는 TElement)
  2. 제네릭 제약 조건을 사용하여 타입 안정성 강화
  3. 필요한 경우에만 제네릭 사용 (과도한 사용은 코드를 복잡하게 만들 수 있음)
  4. 기본 타입을 제공하여 사용자 편의성 향상
  5. 타입 추론을 최대한 활용하되, 필요시 명시적 타입 인자 제공
  6. 제네릭 함수 테스트 시 다양한 타입으로 테스트하여 견고성 확보

 제네릭 함수는 타입스크립트에서 타입 안정성과 코드 재사용성을 동시에 달성할 수 있는 강력한 도구입니다. 제네릭을 사용함으로써 다양한 타입에 대해 동작하는 유연한 함수를 작성할 수 있으며, 이는 코드의 중복을 줄이고 유지보수성을 높입니다.

 제네릭 함수의 기본 문법은 간단하지만, 그 응용은 매우 다양합니다. 단일 타입 매개변수부터 시작하여 여러 타입 매개변수를 사용하는 복잡한 함수까지 구현할 수 있습니다. 이를 통해 함수의 입력과 출력 타입 간의 관계를 정확하게 표현할 수 있습니다.

 제네릭 제약조건은 타입 매개변수가 가져야 할 특정 속성이나 메서드를 지정할 수 있게 해줍니다. 이는 함수 내에서 해당 속성이나 메서드를 안전하게 사용할 수 있게 하며, 타입 오류를 컴파일 시간에 잡아낼 수 있습니다.

 keyof 연산자와 제네릭을 함께 사용하면 객체의 속성에 타입 안전하게 접근할 수 있습니다. 이는 특히 동적 속성 접근이 필요한 경우에 유용합니다.

 제네릭 함수에 기본 타입을 지정하면 사용자가 명시적으로 타입을 지정하지 않아도 되는 경우의 편의성을 제공할 수 있습니다. 이는 API의 사용성을 높이는 데 도움이 됩니다.

 조건부 타입과 제네릭을 결합하면 매우 복잡한 타입 로직을 표현할 수 있습니다. 이는 고급 타입 연산을 수행하거나 타입 기반의 분기 로직을 구현할 때 유용합니다.

 타입스크립트의 타입 추론 메커니즘은 대부분의 경우 제네릭 함수의 타입을 정확히 추론할 수 있습니다. 그러나 복잡한 경우나 더 명확한 타입 정보가 필요한 경우에는 명시적으로 타입 인자를 제공할 수 있습니다.

 제네릭 함수 작성 시 흔히 발생하는 오류로는 제약 조건 관련 오류와 타입 매개변수 이름 충돌 등이 있습니다. 이러한 오류는 주의 깊은 설계와 적절한 제약 조건 사용으로 해결할 수 있습니다.

 효과적인 제네릭 함수 설계를 위해서는 몇 가지 Best Practices를 따르는 것이 좋습니다. 의미 있는 타입 매개변수 이름 사용, 적절한 제약 조건 적용, 필요한 경우에만 제네릭 사용, 기본 타입 제공 등이 여기에 포함됩니다. 또한, 다양한 타입으로 함수를 테스트하여 견고성을 확보하는 것도 중요합니다.

 결론적으로, 제네릭 함수는 타입스크립트에서 타입 안전성과 코드 재사용성을 크게 향상시킬 수 있는 강력한 도구입니다. 적절히 사용하면 더 유연하고 견고한 코드를 작성할 수 있으며, 이는 대규모 애플리케이션 개발에서 특히 중요합니다. 그러나 과도한 사용은 코드를 불필요하게 복잡하게 만들 수 있으므로, 상황에 맞게 적절히 사용하는 것이 중요합니다.