ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Typescript Generic Function (제네릭 함수)
    카테고리 없음 2022. 3. 30. 22:44

    Generic은 Java, C#과 같은 객체지향 프로그래밍에서 사용하는 기법이다.

     

    제네릭은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법이다.

    쉽게 말하면 메서드 매개변수의 구체적인 타입을 기재하지 않고 다양한 타입을 처리할 수 있는 기술이며, 잘만 사용한다면 코드의 재사용성을 높일 수 있다.

     

    원래 javascripts는 타입 선언이 필요하지 않고, 그렇기에 특정 타입을 위해 만들어진 클래스나 함수도 타입에러를 런타임에서 일으킬 뿐이다.

    그래서 javascript는 제네릭이란 말을 들을 일이 없다.

     

    그래서 이런 타입 기반언어에서 generic을 사용하는데 Typescript에서도 사용할 수 있으며 함수, 인터페이스, 클래스의 재사용성을 높일 수 있다.

     

     

    Generic을 Class에 사용


    아래는 간단하게 Stack을 구현한 클래스이다.

    class Stack {
        private stack: any[] = [];
    
        constructor() {}
    
        push(item: any): void {
            this.stack.push(item)
        }
    
        pop(): any{
            return this.stack.pop();
        }
    }

    자료구조는 보통 범용적으로 타입을 수용할 수 있더록 만들어야 한다.

    그래서 typescript는 any를 이용하여 구현할 수 있는게 가장 쉽다.

     

    하지만 문제는 아래에서 발생한다.

    const stack = new Stack();
    stack.push(1);
    stack.push('a');
    stack.pop().substring(0); // 'a'
    stack.pop().substring(0); // Type Error

     

    그러면 타입에 딱 맞게 스택들을 만드려면 다음과 같이 구현을 해야한다.

    class NumberStack extends Stack {
        constructor() {
            super();
        }
        
        push(item: number) {
            super.push(item);
        }
        
        pop(): number {
            return super.pop();
        }
    }

    하지만 이렇게 되면 문제점은 새로운 자료형을 추가할 때 마다 클래스를 계속 추가해줘야 한다.

     

    여기서 제네릭이 빛을 발한다.

    class Stack<T> {
        private stack: T[] = [];
    
        constructor() {}
    
        push(item: T): void {
            this.stack.push(item)
        }
    
        pop(): T{
            return this.stack.pop();
        }
    }

    T는 Type의 약자로 다른 언어에서도 제네릭을 선얼할 때 많이 사용 된다.

    여기서 T를 타입변수(Type variables)라고 한다.

    이렇게해서 클래스에서 제네릭을 사용하겠다고 선언한 경우 T는 해당 클래스에서 사용할 수 있는 특정한 타입이 된다.

     

    const numStack = new Stack<number>();
    numStack.push(1);
    
    const strStack = new Stack<string>();
    strStack.push('a')

    이렇게 class 를 선언하면 하나의 클래스를 통해 여러가지 타입을 다룰 수 있다.

     

     

    Generic을 함수에 사용


    기본적인 사용

     

    배열을 받아서 배열의 첫번째 요소를 리턴하는 함수를 만든다고 가정을 했을 때 제네릭을 안쓰고 구현 하면 다음과 같이 될 것이다.

    function first (arr: any[]): any{
        return arr[0]
    }

    위의 방식은 클래스와 했던 방식과 마찬가지로 모든 타입의 배열을 받기 때문에 리턴하게 되는 것이 무슨 타입인지 알 수 없다.

     

    이것을 해결하기 위해서 아래와 같이 제네릭을 쓰면 된다.

    function first<T> (arr: T[]): T{
        return arr[0]
    }

    클래스에서 했던 방식과 동일하게 함수명 오른쪽에 <T> 키워드를 붙혀주고 특정 타입을 T로 지정해준다.

     

    사용할 때는 다음과 같이 사용한다.

    const number = first<number>([1,2,3]) //1

     

    두 개 이상의 타입 변수

    제네릭 함수나 클래스에서는 두개 이상의 타입 변수도 사용 할 수 있다.

    function toPair(a: any, b: any): [ any, any]{
        return [a ,b];
    }

    위의 코드에서는 두개의 변수 타입이 다르다고 가정하고 두가지 타입 변수가 필요하게 된다.

     

    이를 제네릭으로 바꾸면 아래와 같은 형식으로 수정 할 수 있다.

    function toPair<T, U>(a: T, b: U): [ T, U ] {
        return [a, b]
    }

    원래는 <T> 만 사용하였다면 <T, U> 두 가지의 타입 변수로 선언 할 수 있다.

     

    해당 함수를 사용하는 방법은 다음과 같이 사용하면 된다.

    toPair<string, number>('1',1);
    toPair<number, number>(1,1);

     

    인터페이스와 Generic

    타입 변수는 기존에 사용하고 있는 타입을 상속할 수도 있다. 이 점을 이용하면 입력 받을 변수의 타입을 제한할 수 있다.

    예를 들어 특정 인터페이스 혹은 타입을 통해 타입을 제한 할 수도 있다.

    interface Item {
        name: string;
        price: number;
        stock: number;
    }
    
    function getItem<T extends Item> (item: T): T{
        return item;
    }

    위와 같은 코드가 있으면 제네릭으로 받는 타입 T는 Item의 하위 타입이다. T는 항상 name, price, stock이 포함 되어야 한다.

    그러면 아래와 같으면 어떨까

    function getItem<T extends keyof Item> (item: T): T{
        return item;
    }

    keyof 타입 을 사용하면 name, price, stock 중에 하나만 들어가면 된다.

    댓글

Designed by Tistory.