9. 제네릭

CHOYEAH·2023년 11월 5일
0

TypeScript

목록 보기
9/23
post-custom-banner

제네릭이란 타입을 마치 함수의 파라미터처럼 사용하여 재사용성이 높은 컴포넌트를 만들 때 활용 특히 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용.

자바스크립트, 유니온, 제네릭 비교


  • 일반 자바스크립트
function logText(text) {
    console.log(text);
    return text;
}
logText(10);
logText('string');
logText(true);

function logText(text: string) {
    console.log(text);
    text.split('').reverse().join('');
    return text;
}

function logNumber(num: number) {
    console.log(num);
    return num;
}

function logBoolean(bool: boolean) {
    console.log(bool);
    return bool;
}

단순히 다른 타입을 인자로 받기 위해서 중복되는 코드를 생산해 나가는것은 가독성 뿐만 아니라 유지보수 관점에서도 좋지 않다.

  • 유니온 사용 시
function logText(text: string | number | boolean) {
    console.log(text);

    //문제1 text.{string, number, boolean 타입의 공통적인 속성만 사용 가능}
    text.valueOf();
    text.toLocaleString();
    text.toString();

    return text;
}

// 문제2 vsc의 랭귀지 서버에서는 리턴값의 타입을 정확히 추론하지 못하기 때문에 
// 아래와 같이 스트링 타입 인자를 통해 스트링 타입의 결과를 받더라도 
// 스트링의 내장 함수 사용 시 에러가 발생된다.
const a = logText('a');
a.split(''); // 에러 발생
  • 제네릭 활용 시
    • 함수에 제네릭 적용
      function logText2<T>(text: T):T {
          console.log(text);
          return text;
      }
      const str = logText2<string>('string');
      str.split('')
      
      const num = logText2<number>(10);
      num.toFixed();
      
      const bool = logText2<boolean>(false);
      if(bool) console

      제네릭을 사용하면 위와같이 인자의 타입을 여러가지 타입으로 적용하여 함수를 재활용할 수 있다.

    • 인터페이스에 제네릭 적용
      interface Dropdown<T> {
          value: T;
          selected: boolean;
      }
      
      const obj1: Dropdown<string> = { value: 'string', selected: false};
      const obj2: Dropdown<number> = { value: 1000, selected: false};
      const obj3: Dropdown<boolean> = { value: true, selected: false};

      인터페이스에 타입을 넘겨주어 값에 해당하는 타입을 유동적으로 적용시킬 수 있다.

제네릭 실습


  • 유니온 타입을 사용했을 경우
    interface Email {
      value: string;
      selected: boolean;
    }
    
    interface ProductNumber {
      value: number;
      selected: boolean;
    }
    
    const emails: Email[] = [
      { value: 'naver.com', selected: true },
      { value: 'gmail.com', selected: false },
      { value: 'hanmail.net', selected: false },
    ];
    
    const numberOfProducts: ProductNumber[] = [
      { value: 1, selected: true },
      { value: 2, selected: false },
      { value: 3, selected: false },
    ];
    
    function createDropdownItem(item: Email | ProductNumber) {
      const option = document.createElement('option');
      option.value = item.value.toString();
      option.innerText = item.value.toString();
      option.selected = item.selected;
      return option;
    }
    
    emails.forEach((email) => {
      const item = createDropdownItem(email);
      const selectTag = document.querySelector('#email-dropdown');
      selectTag.appendChild(item);
    });
    
    numberOfProducts.forEach((product) => {
      const item = createDropdownItem(product);
      const selectTag = document.querySelector('#product-dropdown');
      selectTag.appendChild(item);
    })

    createDropdownItem()에 유니온을 사용하여 두 개의 인터페이스를 인자 타입으로 사용할 수 있게하였다. 결과적으로 타입이 다른 emails, numberOfProducts 배열을 루프시켜 createDropdownItem() 함수를 타입에 관계없이 활용할 수 있다.

  • 제네릭을 활용한 경우
    
    // interface Email {
    //   value: string;
    //   selected: boolean;
    // }
    
    // interface ProductNumber {
    //   value: number;
    //   selected: boolean;
    // }
    
    interface DropdownItem<T> {
      value: T;
      selected: boolean;
    }
    
    const emails: DropdownItem<string>[] = [
      { value: 'naver.com', selected: true },
      { value: 'gmail.com', selected: false },
      { value: 'hanmail.net', selected: false },
    ];
    
    const numberOfProducts: DropdownItem<number>[] = [
      { value: 1, selected: true },
      { value: 2, selected: false },
      { value: 3, selected: false },
    ];
    
    function createDropdownItem(item: DropdownItem<string> | DropdownItem<number>) {
      const option = document.createElement('option');
      option.value = item.value.toString();
      option.innerText = item.value.toString();
      option.selected = item.selected;
      return option;
    }
    
    // 유니온을 사용하지 않고 함수에도 제네릭을 적용시켜 드랍다운 아이템에서도 타입을 참조하게끔 할 수 있다.
    // function createDropdownItem<T>(item: DropdownItem<T>) {
    //   const option = document.createElement('option');
    //   option.value = item.value.toString();
    //   option.innerText = item.value.toString();
    //   option.selected = item.selected;
    //   return option;
    // }
    
    emails.forEach((email) => {
      const item = createDropdownItem(email);
      // const item = createDropdownItem<string>(email);
      const selectTag = document.querySelector('#email-dropdown');
      selectTag.appendChild(item);
    });
    
    numberOfProducts.forEach((product) => {
      const item = createDropdownItem(product);
    	// const item = createDropdownItem<number>(product);
      const selectTag = document.querySelector('#product-dropdown');
      selectTag.appendChild(item);
    })

제네릭의 타입 제한


타입 제한을 적절히 사용하면 제네릭을 목적하는 곳에 타입을 제한시켜 제네릭을 보다 안전하게 쓸 수 있다.

  • 문제상황 예시
    function logTextLength<T>(text: T): T {
      console.log(text.length); // 에러 발생 
    	return text;
    }
    

    타입스크립트 입장에서는 text가 어떤 타입인지 알 수 없다. 따라서 .length를 사용 시 에러가 발생된다.

  1. 제네릭에 타입을 추가적으로 명시하여 타입 제한

    제네릭에 배열 타입을 추가적으로 명시하여 타입에 대한 힌트를 제공함으로써 타입을 제한할 수 있다.

    // 배열([]) 타입을 추가적으로 명시
    function logTextLength<T>(text: T[]): T {
      console.log(text.length);
      return text;
    }

    제네릭에 배열([]) 타입을 추가적으로 명시해주면 타입스크립트는 인자가 배열임을 알 수 있으므로 length 사용 시 에러가 발생되지 않는다.

  2. 정의된 타입으로 타입 제한

    interface LengthType {
      length: number;
    }
    
    // 인터페이스를 상속 받는 제네릭으로 명시
    function logTextLength<T extends LengthType>(text: T): T {
      console.log(text.length); 
      return text;
    }
    
    logTextLength<string>('hi');
    logTextLength<number>(10); // number 타입에는 length 속성을 가지고있지 않으므로 오류가 발생한다.

    string은 length 속성을 가지고 있다. length 속성을 보장하는 인터페이스를 상속하는 제네릭으로 명시하였으므로 length 속성을 사용할 수 있다. 단, 함수 호출시에 number 타입과 같이 length 속성을 가지고있지 않은 타입으로 호출 시 오류가 발생한다.

  3. keyof로 제네릭의 타입 제한

    interface ShoppingItem {
      name: string;
      price: number;
      stock: number;
    }
    
    function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
      return itemOption;
    }
    
    getShoppingItemOption('name')
    getShoppingItemOption('price');
    getShoppingItemOption('stock');
    getShoppingItemOption('something') // 오류 발생

    keyof 키워드를 사용하여 특정 인터페이스의 정의된 키값으로만 인자를 제한할 수 있다.

profile
Move fast & break things
post-custom-banner

0개의 댓글