Published on

Typescript 정리 -Generic

글쓴이

    📌 목차

    typescript

    Generic이란?

    generic은 "일반적인"이라는 의미를 가진 general의 유사어이다.

    Generic [adj]
    You use generic to describe something that refers or relates to a whole class of similar things. Synonyms: collective, general, common, wide

    프로그래밍 언어에서의 일반적인 타입은 number, string 등 특정 타입에 종속되지 않은 일반화된 타입의 맥락으로 사용된다. 조금 과격하게 표현하면, 타입이 무엇이어도 상관없는 포용성있는 타입이 generic type이다.

    내장 제네릭

    타입스크립트의 내장 제네릭 타입 : Array

    const names: Array = []; // error : Generic type 'Array<T>' requires 1 type argument(s).
    const names2: Array<string> = ['a', 'b'];
    names2.toString(); // 'a','b'
    

    타입스크립트의 Array는 제네릭 타입이다. 제네릭 타입에 특정한 타입을 지정해 명시된 type 정보를 토대로 타입에 알맞은 처리를 해줄 수 있다.

    자바스크립트의 제네릭 타입 : Promise

    const promise: Promise<number> = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(10)
      }, 1000);
    })
    promise.then((data) => {
      data += 10;
    })
    

    자바스크립트의 Promise도 제네릭 타입이지만, 결국 리턴될 타입을 명시해준다면 보다 적절하고 type-safe한 처리를 할 수 있다.

    제네릭 함수 정의하기

    function merge<T,U> (objA : T, objB : U) {
        return Object.assign(objA, objB);
    }
    const mergedObj = merge({name: 'Tina'}, {age:30});
    console.log(mergedObj); // [LOG]: {  "name": "Tina",  "age": 30} 
    const mergedObj2 = merge({car :'승용차'}, { cycle : 'MTB'}); 
    console.log(mergedObj2);// [LOG]: { "car": "승용차", "cycle": "MTB"} 
    

    위와 같이 merge 함수를 정의하면, 타입스크립트에서는 다양한 타입을 받아들이겠다는 것을 인식한다. 재사용성이 높은 코드를 작성할 수 있는 것이 제네릭 타입의 큰 장점이다.

    제네릭을 정의할때 :
    일반적으로 알파벳 대문자 하나로 명시하고, 파라미터가 하나일때는 T(Type)를 사용한다.

    제약조건 설정하기

    앞서 작성한 제네릭 함수는 객체가 아닌 number, string 같은 primitive 타입을 입력하면 문제가 발생한다. Object.assign 함수는 객체에서 대해서만 처리할 수 있기 때문이다.

    extends

    function merge<T extends object, U extends object>(objA: T, objB: U) {
      return Object.assign(objA, objB);
    }
    
    const mergedObj = merge({ name: 'Max', hobbies: ['Sports'] }, { age: 30 });
    console.log(mergedObj);
    

    무한정으로 타입을 유연하게 사용하는 것이 아니라, T extends object과 같이 제약조건을 설정해서 type-safe한 처리를 할 수 있다.

    interface Lengthy {
      length: number;
    }
    
    function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
      let descriptionText = 'Got no value.';
      if (element.length === 1) {
        descriptionText = 'Got 1 element.';
      } else if (element.length > 1) {
        descriptionText = 'Got ' + element.length + ' elements.';
      }
      return [element, descriptionText];
    }
    
    console.log(countAndDescribe(['Sports', 'Cooking'])); // [["Sports", "Cooking"], "Got 2 elements."] 
    console.log(countAndDescribe('Hi there!')); // ["Hi there!", "Got 9 elements."] 
    console.log(countAndDescribe(10)); // Argument of type 'number' is not assignable to parameter of type 'Lengthy'.
    

    위의 코드도 마찬가지이다. type이나 interface를 사용해 타입을 정의하고, 제네릭 타입 TLengthy를 상속하도록 설정하였다. 그러면, countAndDescribe에 인자로 넘어가는 element는 반드시 number 타입인 length 요소를 가져야한다. number 타입인 10은 length를 요소로 가지지 않기 때문에 에러가 발생한다.

    타입스크립트에서는 countAndDescribe(10)에서 문제가 발생하지만, 자바스크립트에서는 발생하지 않고, 정상출력된다. -> [10, "Got no value."]

    keyof

    function extractAndConvert<T extends object, U extends keyof T>(
      obj: T,
      key: U
    ) {
      return 'Value: ' + obj[key];
    }
    
    extractAndConvert({ name: 'Tina' , gender : 'Female'}, 'name');
    

    keyof 는 source가 되는 타입의 키를 모아 유니언 타입으로 만들어주는 키워드이다. 위 코드의 Ukeyof T를 상속 받으므로, 두번째 파라미터값이 'name' 또는 'gender'이면 에러가 발생하지 않는다. keyof는 없는 키값에 접근하는 경우를 방지 할 수 있기 때문에 유용하다.

    Generic class

    class DataStorage<T> {
      private data: T[] = [];
    
      addItem(item: T) {
        this.data.push(item);
      }
    
      removeItem(item: T) {
        if (this.data.indexOf(item) === -1) {
          return;
        }
        this.data.splice(this.data.indexOf(item), 1); // -1
      }
    
      getItems() {
        return [...this.data];
      }
    }
    
    const textStorage = new DataStorage<string>();
    textStorage.addItem('a');
    textStorage.addItem('b');
    textStorage.removeItem('a');
    console.log(textStorage.getItems());
    
    const numberStorage = new DataStorage<number>();
    numberStorage.addItem(1);
    numberStorage.addItem(2);
    numberStorage.removeItem(2);
    console.log(numberStorage.getItems());
    
    const objStorage = new DataStorage<object>();
    const human = {name: 'Tina'};
    //objStorage.addItem({name: 'Tina'});
    //objStorage.removeItem({name: 'Tina'});
    objStorage.addItem(human);
    objStorage.removeItem(human);
    console.log(objStorage.getItems());
    

    제네릭 함수 뿐 아니라 제네릭 클래스도 만들 수 있다. 다만 위 코드는 object를 원소 타입으로 사용할 때 주의를 기울여야 한다. object는 참조타입이므로 주석처리된 두개의 object를 다른 것으로 인식하고, 항상 마지막 원소를 삭제하게 된다는 위험성이 있다. 동일한 원소를 삭제하기 위해서는, 변수를 선언하고, 해당 변수를 인자로 넘기면 해결할 수 있다.

    아니면 클래스의 타입을 <T extends string | number | boolean>와 같이 한정해주는 처리도 가능하다.

    object는 Call by Reference라는 사실에 유의한다.

    Generic Utility Type

    Partial

    특정 타입의 모든 속성들을 Optional 로 만들어주는 타입스크립트의 제네릭 타입이다. 아래 코드는 Todo 타입인 변수의 일부 값을 update 시키기 위해 Partial타입을 사용해줬다. description만 지정해서 넘겨줘도 문제가 일어나지 않았다.

    interface Todo {
      title: string;
      description: string;
    }
     
    function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
      return { ...todo, ...fieldsToUpdate };
    }
     
    const todo1 = {
      title: "organize desk",
      description: "clear clutter",
    };
     
    const todo2 = updateTodo(todo1, {
      description: "throw out trash",
    });
    

    Readonly

    Readonly는 한번 값을 할당하면 수정할 수 없다는 점에서 상수와 유사하다. Readonly는 객체의 모든 하위 속성에 적용될 수 있다는 점이 다르다. Readonly를 사용하면 모든 속성값을 수정할 수 없다. 바닐라 자바스크립트의 Object.freeze 같은 기능을 수행한다.

    const names : Readonly<string[]> = ['Tina', 'Dori','Theia'];
    names.push('Name'); // Property 'push' does not exist on type 'readonly string[]'.
    names.pop() ; // Property 'pop' does not exist on type 'readonly string[]'.
    
    interface Todo {
      title: string;
    }
     
    const todo: Readonly<Todo> = {
      title: "Delete inactive users",
    };
     
    todo.title = "Hello"; // Cannot assign to 'title' because it is a read-only property.
    

    Generic Type vs Union Type

    Generic Type은 compile 타임에 사용자가 타입을 명시하고, 타입이 뭘로 명시되든지 관련해서 공통된 처리를 하기 위한 것이다. Union Type은 명시된 여러가지의 타입을 받아들이기 위한 것으로, 공통된 처리보다는 타입별로 분기처리하는 것이 일반적이다.

    Reference