ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] 제네릭
    codeStates front-end/Typescript 2023. 3. 23. 21:52
    반응형

     

     

    목차

       

       

       

      📌  제네릭

       

      알려진 타입은 사용하는 것이 아닌 코드에서 호출하는 방식에 따라 다양한 타입으로 작동하도록 하려면 어떻게 해야 할까?

       

      🙋‍♂️ 타입을 선언하지 않는 다면? any로 간주👉🏻👉🏻 any는 모든 타입을 뜻하는게 아니라 제한을 두지 않는 것일 뿐 👉🏻👉🏻 엄격한 검사를 하지 않기 때문에 문제가 발생할 수 있다.

      🙋‍♂️ 모든 타입을 허용하려면? 제네릭 사용 👉🏻👉🏻 타입 간의 관계를 알아낸다.

      🙋‍♂️ 제네릭이란? 타입을 마치 함수 파라미터로 사용한다는 것, 코드를 수행될 때(런타임) 타입을 명시

                              any처럼 타입을 지정하지 않지만, 타입을 체크해 컴파일러가 오류를 찾을 수 있다.

                              재사용성이 높은 컴포넌트를 만들 때 자주 활용

                              

       

      📍 제네릭 함수

       

      매개변수 괄호 바로 앞 <. >로 묶인 타입 매게변수에 별칭을 배치해 함수를 제네릭으로 만든다.

      함수를 호출할 때 함수 안에서 사용할 타입을 넘겨줄 수 있다.

       

      // Import stylesheets
      import './style.css';
      
      // Write TypeScript code!
      function identity<T>(input : T){
        return input;
      }
      
      const numeric = identity("me");
      const stringy = identity(123);
      // 에러 없이 콘솔 값이 출력되는 것을 알 수 있다.
      console.log(numeric); // string
      console.log(stringy); // number

       

      화살표 함수로 제네릭 선언

       

      // 화살표 함수는 JSX 구문과 충돌하므로 일부 제한이 있다.
      const identity = <T>(input : T) => input;
      
      identity(123);

       

       

      🔗 명시적 제네릭 호출 타입

      제네릭 타입 인수 유추   👉🏻👉🏻 함수가 호출 되는 방식

      Ex) 위 코드인 identity 함수는 indentity 제공된 인수를 사용해 해당 함수 매게변수의 타입 인수를 유추

             identity(123);  👉🏻👉🏻 number

      But!! 함수 정보가 충분히 제공되지 않을 때가 있다. 🙋‍♂️ 어느 경우?? 타입 인수를 알 수 없는 제네릭 구문이 다른 제네릭 구문에 제공된 경우

       

      👇🏻👇🏻👇🏻

       

      function logWrapper<Input>(callback : (input:Input) => void){
        return(input:Input) => {
          console.log("Input",input);
          callback(input);
        }
      }
      
      // type : (input : string) -> void
      logWrapper((input : string) => {
        console.log(input.length);
      });
      
      // type : (input : unknown) -> void
      logWrapper((input) => {
        console.log(input.length); // Error why? input이 무슨 타입인지 알 수 없다.
      });

       

       

      🙋‍♂️ 기본값 unknown을 막기 위해 무엇을 쓸까? 명시적 제네릭 타입 인수

      위 함수를 보면 Input 타입은 string으로 제공 🙏🏼 만약 boolean 값을 넣으면 Error 🙏🏼

      허나 명시적 제네릭 함수는 항상 필요하지는 않다.

       

       

      다중 함수 타입 매게변수

       

      임의의 수의 타입 매게변수를 쉼표로 구분해 함수를 정의

      다중 처리 경우 각 개수를 모두 명시적으로 지정하거나 지정하지 않아야 한다.

      아래의 경우 하나만 명시적으로 지정할 경우 error 가 난다.

       

      function makePair<Key, Value>(key : Key, value : Value){ return {key,value};
      }
      
      makePair("abc",123);
      
      makePair<string, number>("abc",123); // ok
      makePair<"abc",123>("abc",123); // ok
      
      makePair<string>("abc",123); // Error

       

       

      🔗 제네릭 인터페이스

       

      인터페이스도 제네릭으로 선언 가능

      🙋‍♂️ 인터페이스란? 두개의 시스템 사이에 상호 간에 정의한 약속 혹은 규칙을 포괄하여 의미, TS에선 객체로만 다룬다.

      TS에서 내장 Array 메서드(배열)는 제너릭 인터페이스로 정의

      배열은 타입 매게변수 T를 사용해 배열 안에 저장된 데이터의 타입을 나타낸다.

       

       

      interface Box<T>{
        inside : T;
      }
      
      let stringBox : Box<string> = {
        inside : "abc",
      }
      
      let numberBox : Box<number> = {
        inside : 123,
      }
      
      let incorrectBox : Box<string> = { 
        inside : false, // Error boolean은 not assignable string
      }

       

       

      유추된 제네릭 인터페이스 타입

       

      제네릭 함수처럼 인터페이스 또한 타입의 유추가 가능하다. 인수의 값이 타입와 일치하지 않으면 타입 오류를 보고한다.

      인터페이스 타입 매게변수를 선언하는 경우 상응하는 타입 인수를 제공해야 한다. 인수를 제공하지 않으면 Error

       

      function getLast<Value>(node : LinkedNode<Value>): Value{
        return node.next ? getLast(node.next) : node.value;
      }
      
      // 유추된 Value 타입 인수 : date
      let lastDate = getLast({
        value : new Date("09-13-1993"),
      });
      
      //유추된 Value 타입 인수 : string
      let lastFruit = getLast({
          next : {
            value : "banana",
          },
            value:"apple",
        });

       

       

       

      🔗  제네릭 클래스

       

      클래스도 멤버에서 사용할 임의의 수의 타입 매게변수를 선언할 수 있다.

       

      class GenericNumber<T> {
         zeroValue: T;
         add: (x: T, y: T) => T;
      
         constructor(v: T, cb: (x: T, y: T) => T) {
            this.zeroValue = v;
            this.add = cb;
         }
      }
      
      let myGenericNumber = new GenericNumber<number>(0, (x, y) => {
         return x + y;
      });
      
      let myGenericString = new GenericNumber<string>('0', (x, y) => {
         return x + y;
      });
      
      myGenericNumber.zeroValue; // 0
      myGenericNumber.add(1, 2); // 3 number + number
      
      myGenericString.zeroValue; // '0'
      myGenericString.add('hello ', 'world'); // 'hello world' stirng + string
      Copy

       

      명시적 제네릭 클래스 타입

       

      클래스 타입이라고 해도 함수 정보가 충분히 제공되지 않을 때 unkown은 피할 수 없다.

      🙋‍♂️ 기본값 unknown을 막기 위해 무엇을 쓸까? 명시적 제네릭 타입 인수

       

       

      class CurriedCallback<Input>{
        #callback: (input : Input) => void;
      
        ...
        
        new CurriedCallback((input) => {
          console.log(input.length);
        }) // Error
      
      }
      
      
      >>>>>>>
      
        new CurriedCallback<string>((input) => {
          console.log(input.length);
        }) // ok

       

       

      🔗  제네릭 클래스 확장

       

      제네릭 클래스또한 확장이 가능하다. 확장 키워드 : extends

       

       

      제네릭 클래스 확장을 이용한 stack 자료구조

       

      class MyArray<A> {
         // 자식으로 부터 제네릭 타입을 받아와 배열 타입을 적용
         constructor(protected items: A[]) {}
      }
      
      class Stack<S> extends MyArray<S> {
         // 저장
         push(item: S) {
            this.items.push(item);
         }
         // 호출
         pop() {
            return this.items.pop();
         }
      }
      
      class Queue<Q> extends MyArray<Q> {
         // 저장
         offer(item: any) {
            this.items.push(item);
         }
         // 추출
         poll() {
            return this.items.shift();
         }
      }
      
      const numberStack = new Stack<number>([]);
      numberStack.push(1);
      numberStack.push(2);
      const data_numberStack = numberStack.pop(); // 2
      
      const stringStack = new Stack<string>([]);
      stringStack.push('a');
      stringStack.push('b');
      const data_stringStack = stringStack.pop(); // 'b'
      
      const numberQueue = new Queue<number>([]);
      numberQueue.offer(1)
      numberQueue.offer(2)
      const data_numberQueue = numberQueue.poll(); // 1
      
      const stringQueue = new Queue<string>([]);
      stringQueue.offer('a');
      stringQueue.offer('b');
      const data_stringQueue = stringQueue.poll(); // 'a'

       

       

       

      🔗  제네릭 인터페이스 구현

       

      제네릭 인터페이스는 제네릭 기본 클래스를 확장하는 것과 유사하게 작동한다.

      기본 인터페이스의 모든 타입 매게변수는 클래스에 선언되어야 한다.

       

       

      // Import stylesheets
      import './style.css';
      
      // Write TypeScript code!
      interface ActingCredit<Role>{
        role : Role;
      }
      
      class MoviePart implements ActingCredit<string>{
        role : string;
        speaking : boolean;
      
      
        constructor(role : string, speaking : boolean){
          this.role = role;
          this.speaking = speaking;
        }
      }
      
        const part = new MoviePart("Miranda", true);
      
        part.role; // string
      
        class IncorrectExtension implements ActingCredit<string>{
          role : boolean; // error why> ActingCredit에서 이미 role을 string으로 선언했기 때문
        }

       

       

      🔗  메서드 제네릭

       

      클래스 메서드는 클래스 인스턴스와 별개로 자체 제네릭 타입을 선언할 수 있다.

      제네릭 클래스 메서드에 대한 가각의 호출은 각 타입 매개변수에 대해 다른 타입 인수를 갖는다.

       

       

       

      //CreatePairFactory 클래스는 key타입을 선언하고 별도의 value 제네릭 타입을 선언하는 creaatePair 메서드를 포함
      class CreatePairFactory<key>{
        key : key;
      
        constructor(key: key){
          this.key;
        }
        // 반환 타입은 {key:Key, value:value}
        createPair<Value>(value:Value){
          return {key:this.key, value};
        }
      }
      
        const factory = new CreatePairFactory("role");
        const numberPair = factory.createPair(10);
        const stringPair = factory.createPair("Sophie");

       

       

      정적 메서드 제네릭

       

       

      🙋‍♂️정적멤버란? 클래스에는 속하지만, 객체 별로 할당되지 않고 클래스의 모든 객체가 공유하는 멤버

      클래스의 정적멤버는 인스턴스 멤버와 구별되고 클래스의 특정 인스턴스와 연결되어 있지 않다.

      정적 클래스 메서드는 자체 타입 매게변수를 선언할 수 있지만, 클래스에 선언된 어떤 타입 매게변수에도 접근할 수 없다.

       

      // BothLogger 클래스는 instanceLog 메서드에 대한 OnInstance타입 매게변수와 정적 메서드 staticLog에 대한 별도의 Onstatic 타입 매게변수를 선언
      class BothLogger<OnInstance>{
        instanceLog(value : OnInstance){
        console.log(value);
        return value;
        }
      
        static staticLog<OnStatic>(value : OnStatic){
          // OnInstance가 선언되어 있으므로, static 메서드에서는 OnInstance 인스턴스에 접근 불가
          let fromInstance : OnInstance; // Error
      
          console.log(value);
          return value;
        }
      }
      
      const logger = new BothLogger<number[]>;
      logger.instanceLog([1,2,3]); // 타입 number[]

       

       

      🔗  제네릭 타입 별칭

       

      🙋‍♂️ 타입 별칭이란? 원시 타입이나 유니언 타입, 튜플 등등 모든 변수에 자유롭게 활용할 수 있는 커스텀 타입

      각 타입 별칭에는 T를 받는 Nullish 타입과 같은 임의의 수의 타입 매게변수가 주어진다.

      제네릭 타입 별칭은 일반적으로 제네릭 함수의 타입을 설명하는 함수와 함께 사용

       

      type Nullish<T> = T | null | undefined;

       

       

      제네릭 판별된 유니언

       

      판별된 유니언 : JS 패턴 + TS 내로잉

      데이터의 성공적인 결과 또는 오류로 인한 실패를 나타내는 제네릭 '결과'타입을 만들기 위해 타입 인수를 추가하는 것

       

       

      // 성공 또는 실패 여부에 대한 결과를 좁히는 데(내로잉)사용하는 succeded 판별자를 포함
      type Result<Data> = FailureResult | SuccessfulResult<Data>;
      
      interface FailureResult {
        error : Error;
        succeeded : false;
      }
      
      interface SuccessfulResult<Data>{
        data : Data;
        succeeded : true;
      }
      
      function handleResult(result : Result<string>){
        if(result.succeeded){
          // result : string 타입
          console.log('we did it! ${result.error}');
        } else{
          // result : FailureResult의 타입 error
          console.error("Awww...${result.error}");
        }
      
        result.data; // Error string X
      }

       

       

      🔗  제네릭 제한자

      TS는 제네릭 타입 매게변수의 동작을 수정하는 구문도 제공

       

       

      제네릭 기본값

       

      기본 클래스로 사용되는 경우 각 타입 매게변수에 대한 타입 인수를 제공해야 한다.

      타입 매게변수 선언 뒤에 =와 기본 타입을 배치해 타입 인수를 명시적으로 제공 할 수 있다.

      기본값은 타입 인수가 명시적으로 선언되지 않고 유추할 수 없는 모든 후속 타입에 사용된다.

       

       

      // 기본값을 string인 T 타입 매게변수를 받는다
      interface Quote<T = string>{
        value : T;
      }
      // 명시적으로 T를 number로 설정
      let explicit : Quote<number> = {value:123};
      // 동일한 선언 안의 타입 매게변수를 기본값으로 가진다 기본값을 설정하면 사용 가능
      let implicit : Quote = {value : " 나는 스트링 ~~ "};
      // 명시적으로 number를 선언해주지 않았기 때문에 Error
      let mismatch : Quote = {value : 123}; // Error

       

      다음 코드에서는 key의 기본값이 없으면 에러가 나는 코드이다.

       

      interface keyValuePair<Key, Value = Key>{
        key : Key;
        value : Value;
      }
      
      let allExplicit : keyValuePair<string, number> ={ 
        key : "rating",
        value : 10,
      };
      
      let oneDefaulting : keyValuePair<string> ={ 
        key : "rating",
        value : "ten",
      };
      
      let firstMissing : keyValuePair = { //error 기본값 X
        key : "rating",
        value : 10,
      }

       

      기본값이 없는 제네릭 타입은 기본값이 있는 제네릭 타입 뒤에 오면 안 된다.

       

      function inTheEnd<First, Second, Third = number, Fourth = string>() {} // ok
      // 기본값이 없는 제네릭 타입이 기본값이 있는 타입 다음에 있으면 에러
      function inTheMiddle<First, Second=boolean, Third = number, Fourth>() {} // Fourth Error

       

       

       

      제한된 제네릭 타입

       

      제네릭 타입 👉🏻👉🏻 클래스, 인터페이스, 윈싯값, 유니언, 별칭 등 모든 타입을 제공

      But!!! 일부 함수는 제한된 타입에서만 작동

      제약 조건이 있는 인터페이스를 만들고, extend 키워드를 이용해 제약 조건을 명시한다

       

      interface LengthWise {
            length: number;
          }
          
          function testFn<T extends LengthWise>(args: T): T {
            return args;
          }
      
          const withLenghObject = {
            length: 2
          }
      
          const withoutLengthObject = {
            age: 3
          }
      
      
          testFn([1,2,3]);
          testFn(['a','b','c']);
          testFn(123); // Argument of type 'number' is not assignable to parameter of type 'LengthWise'.
          testFn(withLenghObject);
          testFn(withoutLengthObject); //  Property 'length' is missing in type '{ age: number; }' but required in type 'LengthWise'.

       

       

      keyof와 제한된 타입 매게변수

       

      🙋‍♂️ 타입 제한자 keyof란? 객체 데이터를 객체 타입으로 변환해주는 연산자

      extends와 keyof를 함께 사용하면 타입 메게변수를 이전 타입 매게변수의 키로 제한할 수 있다.

      또한 제네릭 타입의 키를 지정하는 유일한 방법이다.

       

       

      // keyof가 없다면 key 매게변수를 올바르게 입력할 방법이 없다
      function get<T, key extends keyof T>(container:T, key:key){
        return container[key];
      }
      
      const roles = {
        favorite : "Fargo",
        others : ["string1" , "string2" , "string3"],
      };
      
      const favorite = get(roles , "favorite"); // type : string
      const others = get(roles , "others"); // type : string[]
      const missing = get(roles, "extras"); // Error type "favorte" | "others"

       

       

      타입 매게변수로 T만 제공되고 key 매게변수가 모든 keyof T일 수 있는 경우라면 반환 타입은 Continer에 있는

      모든 속성 값에 대한 유니언 타입이 된다.

      🙋‍♂️유니언 타입이란? 자바스크립트의 OR 연산자(||)와 같이 A이거나 B이다 라는 의미의 타입

       

       

      function get<T>(container : T, key : keyof T){
        return container[key];
      }
      
      const roles = {
        favorite : "Fargo",
         others : ["string1" , "string2" , "string3"],
      }
      
      const found = get(roles, "favorite"); // type : string | string[]

       

       

      🔗  Promise

       

      최신 JS의 핵심 기능인 Promise, Promise를 사용하려면 제네릭과는 뗄 수 없는 사이이다.

      Promise는 대기/완료/거부 상태를 가진다. 그러므로 생성되었을 때는 완료 후에 어떤 값을 가지게 될지 알 수 없다. 그러므로 제너릭을 통해 어떤 형태의 반환값을 가질지 미리 명시해줘야 한다.

      각 Promise는 대기 중인 작업이 완료 또는 오류가 발생하는 경우 콜백을 등록하기 위한 메서드를 제공한다.

       

       

      Promise 생성

       

       

      1. 프로미스 생성자에 대해 new를 호출해서, 프로미스 생성자가 상태를 결정하기 위해 resolvereject 함수를 전달받는다.

       

      const promise = new Promise((resolve, reject) => {
          // the resolve / reject functions control the fate of the promise
      })

       

      2. 프로미스는 .then을 이용해서 resolved를 처리하고 catch를 사용해서 rejected를 할 수 있다.

       

       

      resolved 처리

       

      const promise = new Promise((resolve, reject) => {
          resolve(123)
      })
      promise.then(res => {
          console.log('I get called:', res === 123) // I get called: true
      })
      promise.catch(err => {
          // This is never called
      })

       

      rejected 처리

       

      const promise = new Promise((resolve, reject) => {
          reject(new Error('Something awful happened'))
      })
      promise.then(res => {
          // This is never called
      })
      promise.catch(err => {
          console.log('I get called:', err.message) // I get called: 'Something awful happened'
      })

       

      타입 스크립트는 Promise의 타입 인수를 명시적으로 선언해야 한다.

      명시적으로 제네릭 타입 인수가 없다면 기본적으로 매게변수 타입으로 unknown으로 가정

       

      // promise 생성자의 타입 인수 : stirng 를 명시적으로 제공
      const resolvesString = new Promise<string>((resolve) => {
        setTimeout(() => resolve("Done!"), 1000);
      })

       

      Promise의 제네릭 .then 메서드는 반환되는 Promis의 resolve된 값을 나타내는 새로운 타입 매게변수를 받는다.

       

      // type : Promise<string>
      const textEventually = new Promise<string>((resolve) => {
        setTimeout(() => resolve("Done!"), 1000);
      })
      // type : Promise<number>
      const lengthEventually = textEventually.then((text) => text.length)

       

       

      🔗  async 함수

       

      JS에서는 async 키워드를 사용해 Promise를 반환

      이 때! Promise를 명시적으로 언급하지 않더라도 async 함수에서 수동으로 선언된 변환 타입은 항상 Promise 타입이 된다.

       

      // type : Promise<string>
      const textEventually = new Promise<string>((resolve) => {
        setTimeout(() => resolve("Done!"), 1000);
      })
      // type : Promise<number>
      const lengthEventually = textEventually.then((text) => text.length)
      
      async function givePromiseForString(): Promise<string> { return "Done!"}; // ok
      
      async function giveString(): string { return "Done!"}; // Error promise<T> type X

       

       

      🔗  제네릭 올바르게 사용하기

       

      제네릭을 과도하게 사용하면 읽기 혼란스럽고 지나치게 복잡한 코드를 만들기도 한다.

      필요할 때만 제네릭을 사용하고, 제네릭을 사용할 때는 무엇을 위해 사용하는지 명확히 해야 한다.

       

       

      제네릭 황금률

       

      함수에 타입 매게변수가 필요한지 여부를 판단할 수 있는 간단하고 빠른 방법은 타입 매게변수가

      최소 두 번 이상 사용되었는지 확인하는 것이다.

      제네릭은 타입 간의 관계를 설명하므로 제네릭 타입 메게변수가 한 곳에만 나타나면 여러 타입 간의 관계를 정의할 수 없다.

       

      // logInput 함수는 input 매게변수를 선언하기 위해 Input 매게변수를 정확히 한 번 사용
      function logInput<Input extends string>(input : Input){
        console.log("Hi", input);
      }

       

      logInput 타입 매게변수로 더 많은 매게변수를 반환 또는 선언하지 않기 때문에 input 타입 매게변수는 선언 작업 불필요

      이럴 경우 다음과 같이 선언하는 것이 좋다.

       

      function logInput(input : string){
        console.log("Hi", input);
      }

       

       

      🔗  제네릭 명명 규칙

       

      타입 매게변수에 대한 표준 명명 규칙

      • 첫 번째 타입 인수로 T를 사용
      • 후속 타입 매게변수가 존재하면 U,V등을 사용
      • 상태관리 라이브러리에서는 제네릭 상태를 S로, 데이터 구조의 키와 값은 와 V로 명명
      • 제네릭의 의도가 단일 문자 T에서 명확하지 않은 경우 타입이 사용되는 용도를 가리키는 설명적인 제네릭 타입 이름을 사용
      • 설명적인 제네릭 타입 이름은 가독성을 위해 완전히 작성된 이름을 사용

       

      반응형

      'codeStates front-end > Typescript' 카테고리의 다른 글

      [learn-typescript] typescript의 장점  (0) 2023.07.03
      [learn-typescript] live server 이용해서 user 정보 가져오기  (0) 2023.06.28
      [TS] 타입 제한자  (0) 2023.03.21
      [TS] 클래스  (0) 2023.03.20
      [TS] 인터페이스  (0) 2023.03.12

      댓글

    Designed by Tistory.