[TS] Generic Type

김채운·2023년 4월 5일
0

Typescript

목록 보기
6/7

➡️ Generic Type?

제네릭은 여러 타입에서 동작하는 재사용성이 높은 컴포넌트를 만들고자 할 때 사용된다. 따라서 타입을 직접적으로 고정된 값으로 고정하기 보다는 언제든지 변할 수 있는 타입을 통해서 보다 유연하게 코드를 작성할 수 있게 해준다. 그래서 사용자는 제네릭을 사용함으로서 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있다.

➡️ Generic Type이 필요한 이유?

add함수는 숫자를 더할 수도 문자를 더할 수도 있는 함수이다. 그래서 타입을 union을 사용해서 add함수로 넘어오는 인자들이 number와 string 타입이 모두 가능하게끔 설정을 해두었다.


function add(x: string | number, y: string | number): string | number {
   return x + y;
}

add(1, 2); // 3
add('hello', 'world'); // 'helloworld'

이렇게 보면 문제가 없어 보이지만, union타입을 사용하다보면 x:string, y:string뿐만 아니라,
x:string, y:number 나 x:number, y:string 도 가능하게 된다. 이러한 경우들 때문에 컴파일러는 에러로 감지하고 add 함수의 return x+y <-- 이 부분에서 '+'연산자를 'string | number' 및 'number | string' 형식에 적용할 수 없습니다. 라는 에러를 알린다.

이를 해결하기 위한 방법으로는,
타입별로 함수를 따로 구분하는 방법이 있다. string 타입의 인자만 받는 add함수 따로, number 타입의 인자만 받는 add함수 따로 이런식으로...

function add(x: string , y: string ): string;
function add(x: number , y: number ): number;
function add(x: any, y: any){
  return x + y;
}

add(1,2); // 3
add('hello', 'world'); // 'helloworld'

이러한 방식을 오버로딩이라고 한다. 이런 방식은 타입의 갯수가 늘어날수록 코드가 길어지게 되기 때문에 가독성에 좋지 않다.

❗오버로딩

  • TypeScript에서는 같은 이름을 가진 함수를 여러 개 정의할 수 있으며 각 함수는 서로 다른 타입을 가지는 매개변수로 정의해야 합니다. 매개변수가 다르며 이름이 동일한 함수를 함수 오버로딩이라고 합니다. 쉽게 말하면 동일한 함수의 타입 선언을 여러번 선언하는 것을 말함.

다른 방법이라고 any 타입을 쓰는 것 또한 좋지 못한 방법이다. 왜냐! any라는 타입은 타입 검사를 하지 않기 때문에 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없다.

이러한 문제점과 한계를 해결하기 위해 제네릭이 있는 것이다.

➡️ Generic Type 사용법

generic은 함수나 클래스의 선언 시점이 아닌, 사용 시점에 타입을 선언하는 방식이다.

// 인수들을 받아서 배열로 만들어주는 메소드
function Array<T>(a: T, b: T): T[] {
  return [a,b];
}

Array<number>(1,2)
Array<string>('hello','world')
Array<string | number>('ㅎ',2)

제네릭은 이런 형태로 함수의 이름 뒤에 를 붙여준다 <>안에 문자는 보통 T를 많이 사용한다. 하지만 이 안에 문자는 사용자 마음대로 지정 가능하다.
그리고 매개변수의 타입, return 반환 값 타입도 다 T로 설정해둔다. 우선 이 T가 무슨 타입인지는 모르겠지만 같은 타입은 전부 하나의 문자로 표현한다. 하지만 제네릭은 함수를 선언할 때 타입이 정해지는 게 아니라, 함수를 사용할 때 타입이 정해지기 때문에 문제없다!
그래서 함수를 사용할 때 함수의 이름 옆에 저렇게 타입을 붙여서 정해주면 된다.

➡️ 제네릭 제약 조건

정의된 타입 사용 (extends)

제네릭 타입인 T는 어떤 타입이든 다 될 수 있다고 했지만, 그렇게 되면 범위가 너무 광범위해진다. 그래서 각 함수에 대해 사용처에 따라서 입력값을 제한해야 되는 상황이 생긴다.

function printLength<T>(arg: T): T {
  console.log(arg.length)
  return arg
}

위의 printLength는 에러가 뜬다. 이유는 제네릭 함수는 사용할 때 타입이 정해지기 때문에 그전에는 타입이 정의가 되지 않는다. 따라서 T타입에 length의 속성이 있는지 없는지 아직은 알 수 없기 때문에 에러가 표시 된다. 그래서 여기서 length 속성을 쓰기 위해서는 T타입에 제약 조건을 거는 방법이 있다.

interface LengthWise {
  length: number;
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length);
  return text;
}
  
logText(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' }); // `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음

이렇게 extends로 조건에 제한을 해주면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.

객체의 속성을 제약하는 방법 (keyof)

const obj = {
    name: 'woony',
    age: 20,
}

const obj2 = {
    animal: '🐶',
}

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
  
console.log(getValue(obj, 'name')); // woony
console.log(getValue(obj, 'age')); // 20
console.log(getValue(obj2, 'animal')); // 🐶

T, K extends keyof T란, T의 key값들(name,age,animal)중 하나가 T,K(제네릭)가 된다는 것이다.
위의 코드에서 T는 매개변수에서 보면 obj가 T라고 되어 있는데 미리 만들어둔 obj, obj2 객체를 말한다. 그리고 매개변수key:K 는 이 객체들의 key값들을 말한다. 정리하자면, 매개변수 obj는 obj와 obj2를 타입으로 받고 key는 key값들로 타입을 받는다는 뜻이다 그래서 return 값의 타입도 T[K]로 객체의 key값을 return하겠다고 타입을 명시한 것이다.
결과를 console로 출력해보면 위의 코드의 주석처리처럼 결과를 확인할 수 있다.

출처

0개의 댓글