티스토리 뷰

제네릭

  • 제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다. 
  • C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징이다. 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는 데 사용된다. 
  • TS에서 제네릭은 클래스, 인터페이스, 함수에서 쓸 수 있음(제네릭 함수, 제네릭 인터페이스, 제네릭 클래스) 

제네릭은 타입에 대한 함수라고 생각하면 된다는 ..제로초님 (선생님의 머리를 갖고싶어요)

function add<T>(x: T, y: T): T { return x + y }
add<number>(1, 2);
add(1, 2);
add<string>('1', '2');
add('1', '2');
add(1, '2');

제네릭 선언 위치 기억하기 

function a<T>() {}
class B<T>() {}
interface C<T> {}
type D<T> = {};
const e = <T>() => {};

제네릭 기본값, extends 

 //제네릭으로 매개변수를 받는 것을 제한할 수 있음(extends 키워드를 통해)
  //제네릭을 여러개 동시에 만들면서 각각 다른 제한을 줄 수 있음
  function add<T extends number, K extends string>(x: T, y: K): T {
    return x + y;
  }
  add(1, 2);
  add("1", "2");
  
// <T extends {...}> // 특정 객체
// <T extends any[]> // 모든 배열
// <T extends keyof any> // string | number | symbol
  • 제네릭이란 함수를 선언할 때말고 함수를 실행할 때 타입이 정해진다. 
  • 아래의 예시처럼 제네릭을 동시에 만들면서 각각 다른 제한(extends를 통해)을 줄 수 있음
  • 매개변수 제한이라는 것의 의미는 아래 예시에서 return x + y;에는 문법 오류가 뜨기 때문임 하지만 매개변수는 제한할 수 있기때문임

함수를 제한하는 제네릭(주로 콜백함수)

  • 모든 함수를 제한할 수도 있는데 이럴 땐 any를 써도 된다. 제한이 없는 경우가 많지는 않아서 잘 안쓸 뿐 
//콜백함수 형태제한
  function add<T extends (a: string) => number>(x: T): T {
    return x;
  }
  add((a) => +a);
  
  // <T extends (...args: any) => any> // 모든 함수

클래스 자체를 넘기고 싶을 때 쓰는 제네릭

  • 클래스의 생성자만 뽑고 싶을 때는 이 제네릭을 쓴다
  function add<T extends abstract new (...args: any) => any>(x: T): T {
    return x;
  }

  //클래스 A 자체가 타입이고 내부에 constructor가 있기 때문에
  class A {
    constructor() {}
  }

  add(A); //new A() 는 에러
  // <T extends abstract new (...args: any) => any> // 생성자 타입(클래스 내부에 있는 constructor의 타입)

매개변수 기본값(ES2015) 사용 시 

  • a: number라고 직접 명시해주지 않아도 추론이 가능하면 a = 3 이렇게만 써도 됨
 const a = (a: number = 3, b: number = 5) => {
    return 3;
  };

  //기본값은 참조형도 가능하다. 객체
  const b = (c: { childNum: number } = { childNum: 3 }) => {
    return c.childNum;
  };

제네릭도 위처럼 기본값을 넣어줄 수 있음 

  • 리액트에서 JSX를 사용시, 화살표함수 제네릭을 사용할 때 에러가 나는 경우가 있는데 제네릭에 기본값(= unknown)을 넣어주면 에러가 사라진다. (extends 혹은 = unknown으로 기본값 넣기)
//TS가 기본값을 추론하지 못할 때 제네릭에 기본값을 넣어준다
//리액트 JSX를 사용할 때,
  // const add = <T extends unknown> extends 사용
  // const add = <T,> 콤마 찍는 것으로도 해결이되지만 명시적이지 않아서 쓰는건 권고되지 않음
  // 제네릭에 기본값 할당하기
  const add = <T = unknown>(x: T, y: T) => ({
    x,
    y,
  });
  const result = add(1, 2);
제네릭 <T>는 인터페이스 명 바로 뒤, 클래스명, 타입 뒤에 붙일 수 있다. 선언 시 이름뒤에 항상 제네릭이 같이 온다. 제네릭은  js변환 시 당연히 사라진다.

lib.es5.d.ts 를 통해 공부하기

//선언 시에는 T자리에 뭐가 올 지 모른다. (제네릭이니까)
//제네릭을 사용할 때 타입이 정해진다.
{
  interface Array<T> {
    forEach(
      callbackfn: (value: T, index: number, array: T[]) => void,
      thisArg?: any
    ): void;
  }
  //사용할 때 T(제네릭)을 number로 지정하면 위에 선언된 T자리가 number로 된다고 생각하면 된다.
  //아래처럼 Array<number>로 직접 명시하는 경우는 ts가 제대로 추론하지 못할 때이다.
  //실행 시 제네릭의 자리에 넣어주는 값을 '타입 파라미터'라고 한다.
  //<number>add(1,2); 는 제네릭이 아니라 강제타입지정이다(as 로 바꾸는). 유의
  const a: Array<number> = [1, 2, 3];
  a.forEach((value) => {
    console.log(value);
  });
  [true, false, true].forEach((value) => {
    console.log(value);
  });
  //string | number | boolean
  ["123", 123, true].forEach((value) => {
    console.log(value);
  });
}

{
  interface Array<T> {
    map<U>(
      callbackfn: (value: T, index: number, array: T[]) => U,
      thisArg?: any
    ): U[];
  }

  //map 분석
  const strings = [1, 2, 3].map((elem) => elem.toString()); // ["1","2","3"]
}
{
  interface Array<T> {
    //필터가 제대로 타입 추론을 못할 경우
    filter(
      predicate: (value: T, index: number, array: T[]) => unknown,
      thisArg?: any
    ): T[];

    filter<S extends T>(
      //T는 number
      predicate: (value: T, index: number, array: T[]) => value is S,
      thisArg?: any
    ): S[];
  }
  //value % 2 가 unknown일지 value is S일지 골라야한다.
  //홀수값만 뽑아내는 식이기 때문에 value is S이다.
  //S는 number

  //타입이 문자열인 애들만 뽑아내고 싶은 것인데 결과를 보면
  //string | number []로 잘못추론하고있다. unknown보단 S로 한번 더 추론하고 있는 타입정의를 가져와서 재정의
  const predicate = (value: string | number): value is string =>
    typeof value === "string";

  // is는 커스텀 타입가드이며 형식조건자이다.
  //위의 식을 재정의해서 다시 filteredResult 의 타입 추론을 보면 string[] 으로 잘 나온다.
  const filteredResult = ["1", 2, "3", 4, "5"].filter(predicate);
}
value is S 에서 is 는 타입가드에서 나온 키워드, 형식조건자라고도 불린다. 

is (타입가드)

문법: parameterName is Type

typeof 같은 걸로 타입을 따져서 분기 처리하는 역할을 TS에서 is으로 쓴다.

function isString(test: any): test is string{
    return typeof test === "string";
}

function example(foo: any){
    if(isString(foo)){
        console.log("it is a string" + foo);
        console.log(foo.length); // string function
    }
}
example("hello world");

커스텀 타입 가드에서 많이 사용된다. 

interface Cat { meow: number }
interface Dog { bow: number }
function catOrDog(a: Cat | Dog): a is Dog {
  if ((a as Cat).meow) { return false }
  return true;
}
const cat: Cat | Dog = { meow: 3 }
if (catOrDog(cat)) {
    console.log(cat.meow);
}
if ('meow' in cat) {
    console.log(cat.meow);
}

as(타입 단언, 강제 형변환?)

개발자가 타입을 지정하지 않아도 ts 컴파일러가 추론이 간으한 타입 추론 기능이 TS에 존재한다. 

이럴 때 as를 쓴다. 

타입 단언은 2종류 

  • <Fish>pet //런타임과 컴파일 단계에서 모두 돌아가고
  • (pet as Fish) // 컴파일 때만 돌아간다.
❌주의사항: 리액트로 개발시 꺽쇠로 타입캐스팅하는 것은 tsx 태그 문법과 비슷하기 때문에 as를 권장한다. 
댓글