• [TS] 타입스크립트 제네릭(Generic)

    2023. 10. 27.

    by. 지은이: 김지은

    728x90

     

    1. Generic이란?

    제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 말한다.

     

    정적 Type 언어는 함수, 클래스 정의 시 type을 선언해야한다.

    일반적인 정적 타입 언어는 함수나 클래스를 정의할 때 타입을 선언해야하지만, 제네릭을 이용해 코드가 수행될 때 타입이 명시되도록 한다.

    function sort<T>(item: T[]): T[] {
        return item.sort();
    }
     
    const nums: number[] = [1, 2, 3, 4];
    const chars: string[] = ["a", "b", "c", "d"];
     
    sort<number>(nums);
    sort<string>(chars);

    위 함수는 제네릭타입 'T'의 배열 item을 매개변수로 받고 배열을 정렬 후 정렬된 배열을 반환한다.

    여기서 'T'는 제네릭 타입 변수로 어떤 데이터 타입이든 될 수 있음을 나타낸다.

    'T'는 Type의 약자로, 추가될 때 마다 U, V... 순으로 추가하는 것이 일반적이며 필드의 첫 글자를 사용하기도 한다.

     

    sort함수를 호출할 때 <number> 또는 <string>과 같은 제네릭 타입을 명시적으로 지정해준다.

    만약 sort함수에 <number> 타입을 지정해놓고 chars배열을 인자로 전달하려고 하면 오류가 발생한다.

     

    2. Generic을 사용하는 이유

    재사용성이 높은 함수와 클래스를 생성할 수 있다.

    • 위에 echo 함수에서 제네릭을 사용하지 않았다면 하나의 함수를 만들어야하지만, 제네릭을 사용하면 하나만 선언해도 여러타입에 동작이 가능하다.

    오류를 쉽게 포착할 수 있다.

    • any타입을 사용하면 컴파일 시 타입을 체크하지 않기 때문에 관련 메소드의 힌트를 사용할 수 없다. 그래서 컴파일 시 컴파일러가 오류를 찾지 못한다.
    • Generic도 any 처럼 미리 타입을 지정하진 않지만, 타입을 체크해 컴파일러가 오류를 찾을 수 있다.

     

    3. Union type

    Union type이란 어떤 타입이 올 지 경우의 수를 고려해서 타입을 명시하는 것으로 '|' 를 사용해서 두 개 이상의 타입을 선언하는 방식이다.

    그러나 주의해야할 점은 변수가 사용될 때, 해당 변수에 할당된 값의 실제 타입이 아닌 선언된 전체 유니온 타입에 따라 동작한다.

    만약 string | number 타입을 가진 변수에 문자열을 할당했다면? 이 변수를 사용할 때 문자열과 숫자의 공통 메소드에만 접근할 수 있다.

     

    const printMessage = (message: string | number) => {
      return message;
    }
    
    const message1 = printMessage(1234);
    const message2 = printMessage("hello world!");
    
    console.log(message1.length); // error: length does not exist on type string | number

    예를 들어 printMessage의 경우 string과 number를 포함한 유니온 타입이기 때문에 string에서만 사용 가능한 length 메소드를 사용할 수 없다.

     

    4 . 제약 조건 (Constraints / keyof)

    원하지 않는 속성에 접근하는 것을 막기 위해 제네릭에 제약조건을 사용할 수 있다.

     

    1. Constraints: 특정 타입들로만 동작하는 제네릭 함수를 만들 때 사용

    2. Keyof: 두 객체를 비교할 때 사용

     

    Constraints

    extends 키워드로 제네릭에 제약조건 설정, 제약 조건을 벗어나는 타입을 선언하면 에러가 발생한다.

    const printMessage = <T extends string | number>(message: T): T => {
      return message;
    }
    
    printMessage<string>("1");
    printMessage<number>(1);
    
    //Error: Type 'boolean' does not satisfy the constraint 'string | number'
    printMessage<boolean>(false);

    마지막 줄을 실행하면 boolean 타입이 string과 number 제약 조건을 충족시키지 않기 때문에 에러가 발생한다.

     

     

    keyof

    const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
      return obj[key];
    }
    
    getProperty({a:1, b:2, c:3}, "a");
    
    // error: Argument of type '"z"'is not assignable to parameter of type'"a"|"b"|“c"
    getProperty({a:1, b:2, c:3}, "z");

    위 코드는 두개의 제네릭 타입을 가지는데 T는 객체, U는 객체의 키를 나타내며 함수는 객체와 키 값을 반환한다.

    제네릭 T는 키 값이 a, b, c 만 존재하는 객체이지만 마지막줄 함수를 호출 할 때 "z" 라는 키가 존재하지 않기 때문에 에러가 발생한다. 

     

    5. 디자인 패턴(Factory Pattern with Generics)

    팩토리 패턴이란 객체를 생성하는 인터페이스만 미리 정의하고 인스턴스를 만드는 것을 서브 클래스가 하는 패턴이다.

    여러개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 입력에 따라 하나의 서브 클래스의 인스턴스를 반환한다.

     

    class CarFactory {
      static getInstance(type: String): Car {
        switch (type) {
          case "bus":
            return new Bus();
          default:
            return new Taxi();
        }
      }
    }

    인스턴스를 생성하는 CarFactory 클래스가 있다고 할 때, 타입이 추가될 때마다 getInstance 메소드에 직접 코드를 추가해야한다.

     

    class CarFactory {
      static getInstance<T extends Car>(type: { new (): T }): T {
        return new type();
      }
    }

    하지만 제네릭을 이용하면 getInstance 메소드가 여러 서브 클래스 타입을 가질 수 있게, 즉 타입을 반환만 할 수 있게 만들고 타입을 넘겨주도록 작성하면 새로운 타입이 추가되어도 getInstance를 수정할 필요가 없다.

     

     

    댓글