icon안동민 개발노트

제네릭 유틸리티 함수 구현


 제네릭 유틸리티 함수는 다양한 타입에 대해 작동할 수 있는 재사용 가능한 함수입니다.

 이를 통해 코드 중복을 줄이고 타입 안정성을 높일 수 있습니다.

배열 조작 유틸리티 함수

 배열을 다루는 제네릭 유틸리티 함수의 예시

function map<T, U>(array: T[], callback: (item: T) => U): U[] {
    return array.map(callback);
}
 
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
    return array.filter(predicate);
}
 
function reduce<T, U>(array: T[], callback: (acc: U, item: T) => U, initialValue: U): U {
    return array.reduce(callback, initialValue);
}
 
// 사용 예
const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, x => x * 2);
const evens = filter(numbers, x => x % 2 === 0);
const sum = reduce(numbers, (acc, x) => acc + x, 0);

객체 조작 유틸리티 함수

 객체를 다루는 제네릭 유틸리티 함수

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    const result = {} as Pick<T, K>;
    keys.forEach(key => result[key] = obj[key]);
    return result;
}
 
function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
    const result = { ...obj };
    keys.forEach(key => delete result[key]);
    return result as Omit<T, K>;
}
 
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}
 
// 사용 예
const person = { name: "Alice", age: 30, city: "New York" };
const nameAndAge = pick(person, ["name", "age"]);
const withoutCity = omit(person, ["city"]);
const merged = merge(person, { job: "Engineer" });

비동기 작업 유틸리티 함수

 비동기 작업을 위한 제네릭 유틸리티 함수

function promisify<T, Args extends any[]>(
    fn: (...args: Args) => T
): (...args: Args) => Promise<T> {
    return (...args: Args) => new Promise<T>((resolve, reject) => {
        fn(...args, (error: any, result: T) => {
            if (error) reject(error);
            else resolve(result);
        });
    });
}
 
async function asyncPipe<T>(...fns: Array<(arg: T) => Promise<T> | T>): Promise<T> {
    return fns.reduce(
        async (v, f) => f(await v),
        Promise.resolve({} as T)
    );
}
 
// 사용 예
const readFileAsync = promisify(fs.readFile);
const processFile = asyncPipe(
    readFileAsync,
    JSON.parse,
    (data: any) => data.title
);

함수형 프로그래밍 유틸리티 함수

 함수형 프로그래밍을 지원하는 제네릭 유틸리티 함수

function curry<Args extends any[], Return>(
    fn: (...args: Args) => Return
): CurriedFunction<Args, Return> {
    return function curried(...args: any[]): any {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return (...moreArgs: any[]) => curried(...args, ...moreArgs);
    } as CurriedFunction<Args, Return>;
}
 
function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
    return fns.reduce((prevFn, nextFn) => 
        value => nextFn(prevFn(value)),
        value => value
    );
}
 
// 사용 예
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)); // 3
 
const double = (x: number) => x * 2;
const addTen = (x: number) => x + 10;
const composedFn = compose(double, addTen);
console.log(composedFn(5)); // 20

타입 변환 유틸리티 함수

 타입 변환을 위한 제네릭 유틸리티 함수

function makePartial<T>(obj: T): Partial<T> {
    return Object.entries(obj).reduce((acc, [key, value]) => {
        if (value !== undefined) acc[key as keyof T] = value;
        return acc;
    }, {} as Partial<T>);
}
 
function makeReadonly<T>(obj: T): Readonly<T> {
    return Object.freeze({ ...obj });
}
 
// 사용 예
const partialPerson = makePartial({ name: "Bob", age: undefined });
const readonlyPerson = makeReadonly({ name: "Charlie", age: 25 });

고급 유틸리티 함수

 제네릭 제약 조건과 조건부 타입을 활용한 고급 유틸리티 함수

function createInstance<T extends new (...args: any[]) => any>(
    ctor: T,
    ...args: ConstructorParameters<T>
): InstanceType<T> {
    return new ctor(...args);
}
 
type FunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
 
function bindMethods<T extends object>(
    obj: T
): { [K in FunctionPropertyNames<T>]: T[K] extends Function ? (...args: Parameters<T[K]>) => ReturnType<T[K]> : never } {
    const result = {} as any;
    for (const key in obj) {
        if (typeof obj[key] === 'function') {
            result[key] = (obj[key] as Function).bind(obj);
        }
    }
    return result;
}
 
// 사용 예
class Person {
    constructor(public name: string) {}
    greet() { console.log(`Hello, ${this.name}!`); }
}
const person = createInstance(Person, "David");
 
const obj = {
    name: "Alice",
    greet() { console.log(`Hello, ${this.name}!`); }
};
const boundMethods = bindMethods(obj);
boundMethods.greet(); // "Hello, Alice!"

단위 테스트와 타입 테스트

 제네릭 유틸리티 함수의 단위 테스트

describe('Generic Utility Functions', () => {
    it('should map array elements', () => {
        const result = map([1, 2, 3], x => x * 2);
        expect(result).toEqual([2, 4, 6]);
    });
 
    it('should pick object properties', () => {
        const obj = { a: 1, b: 2, c: 3 };
        const result = pick(obj, ['a', 'c']);
        expect(result).toEqual({ a: 1, c: 3 });
    });
});

 타입 테스트는 TypeScript의 expectType 유틸리티를 사용할 수 있습니다.

import { expectType } from 'tsd';
 
expectType<number[]>(map([1, 2, 3], x => x * 2));
expectType<{ a: number, c: number }>(pick({ a: 1, b: 2, c: 3 }, ['a', 'c']));

성능 최적화와 메모리 사용

  1. 불필요한 객체 생성 피하기 : 가능한 경우 원본 객체를 변경하지 않고 참조를 반환합니다.
  2. 메모이제이션 활용 : 비용이 큰 연산 결과를 캐시하여 재사용합니다.
  3. 지연 평가 : 필요한 시점까지 연산을 미루어 불필요한 계산을 피합니다.

Best Practices와 주의사항

  1. 타입 매개변수 명명 : 의미 있는 이름을 사용하여 가독성을 높입니다.
  2. 제약 조건 활용 : 필요한 경우 제네릭 타입에 제약 조건을 적용하여 타입 안정성을 높입니다.
  3. 문서화 : 복잡한 제네릭 유틸리티 함수는 주석을 통해 사용 방법과 예시를 제공합니다.
  4. 단위 테스트 : 다양한 타입과 시나리오에 대한 테스트를 작성합니다.
  5. 타입 추론 활용 : 가능한 경우 명시적 타입 선언보다 타입 추론을 활용합니다.
  6. 과도한 추상화 피하기 : 실제 필요한 경우에만 제네릭을 사용합니다.
  7. 성능 고려 : 복잡한 타입 연산이 컴파일 시간에 미치는 영향을 고려합니다.
  8. 버전 관리 : TypeScript 버전 업그레이드 시 제네릭 관련 변경사항을 확인합니다.
  9. 에러 처리 : 적절한 에러 처리와 타입 가드를 사용하여 런타임 안정성을 보장합니다.
  10. 재사용성 : 범용적으로 사용할 수 있는 유틸리티 함수를 설계합니다.