typescript

제네릭

냠냠맨 2023. 4. 17. 23:46

🐕 제네릭

https://ko.wikipedia.org/wiki/%EC%A0%9C%EB%84%A4%EB%A6%AD_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D

 

제네릭 프로그래밍 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org

제네릭 프로그래밍은 데이터 형식에 의존하지 않고

하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어

재사용성을 높일 수 있는 프로그래밍 방식이다.

 

hmm 그러니까 값은 하나지만 데이터타입은 여러개를 가질 수 있다가 핵심인가보네요

제네릭은 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 수 있게해준다고 하네요

 


👻what if 제네릭이 없는 세계관

function identity(arg: any): any {
    return arg;
}

제네릭이 없는 세계관에서는 any 타입을 사용할 수 있습니다.

any는 어떤 타입이든 받을 수 있다는 점에서 제네릭하지만..

실제로 함수가 반환할 때 어떤 타입인지에 대한 정보는 잃게 됩니다.

number 타입을 인자로 넘겨줘도 any 타입이 반환된다는 정보만 얻게될 뿐입니다.

 

따라서 무엇이 반환되는지 궁금한 사람은 인수의 타입을 캡쳐할 방법이 필요하고

그 방법은 일단 any 타입을 사용하는 것은 아니겠네요

function identity<T>(arg: T): T {
    return arg;
}

const identity = <T>(arg: T): T => {
  return arg;
};

이거 화살표 함수에 타입을 적는게 안익숙해서 자꾸 헤매게되네요

함수의 경우엔 매개변수를 넣는 소괄호칸 근처에서 모든걸 해결한다고 일단 외우겠습니다.

 

identity에 <T>라는 타입 변수를 추가했습니다.

T는 유저가 준 인수의 타입을 캡처하고 이 정보를 나중에 사용할 수 있게 합니다.

위 예제에서는 우리가 캡처한 인수의 타입을 함수의 반환값으로 사용합니다.

이렇게 인수의 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있고

이를 이용해서 타입 정보를 함수의 매개변수에서 반환값으로 운반할 수 있게 만들어줍니다!

 

이러한 버전의 identity 함수는 타입을 불문하고 동작하니까 제네릭이라고 할 수 있습니다.

하지만 any 타입을 사용하는 것과는 다르게 반환값의 타입도 정확하게 가져갈 수 있습니다.

이렇게 제네릭 identity 함수를 작성하고 나면 두가지 방법으로 호출할 수 있어요!

let output = identity<string>('myString');
let output2 = identity("myString")

하나는 타입을 지정해주는 방법이고

하나는 타입스크립트의 타입추론을 믿는 방법입니다.

보통 두번째 방법이 일반적으로 사용된다고하네요

타입 인수 추론의 경우엔 전달하는 인수에 따라서 컴파일러가 T의 값을 자동으로 정해주게 된다고 합니다.


🥶 제네릭타입 변수 작업(Working with Generic Type Variables)

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length); // 오류: T에는 .length 가 없습니다.
  return arg;
}

그런데 이런 경우에는 문제가 생길 수 있습니다.

우리가 정의한 T 자료형에는 length 프로퍼티가 없기 때문이에요

이런 경우엔 length 프로퍼티를 사용하고 싶은데 어떻게 하면 좋을까요?

function logText<T>(text: T[]): T[] {
  console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용합니다.
  return text;
}

이렇게 작성하면 됩니다!

T[] 부분을 주목해서 살펴보면 이 제네릭 함수 코드는

T라는 변수 타입을 받은 뒤 인자 값으로는 배열 형태의 T를 받습니다.

이렇게 코드를 작성하면 text의 변수로는 배열이 들어가야합니다.

function logText<T>(text: T[]): T[] {
  console.log(text.length);
  return text;
}

console.log(logText([123]));

즉 이런식으로 인수를 넣어줘야 오류없이 동작하게됩니다.

그리고 배열은 항상 length 프로퍼티가 있으니

언제나 length 프로퍼티를 사용할 수 있게 됩니다.

 

이렇게 배열 형태를 활용하면 유연한 방식으로 제네릭에 타입을 줄 수 있습니다.

function logText<T>(text: Array<T>): Array<T> {
  console.log(text.length);
  return text;
}
console.log(logText([123]));

이렇게 작성해도 똑같이 동작합니다.


🌞제네릭 인터페이스 작성해보기

interface GenericLogTextFn {
  <T>(text: T): T;
}
function logText<T>(text: T): T {
  return text;
}
let myString: GenericLogTextFn = logText; // Okay

interface GenericLogTextFn<T> {
  (text: T): T;
}
function logText<T>(text: T): T {
  return text;
}
let myString: GenericLogTextFn<string> = logText;

자유도가 높네요...

편한대로 작성하면 되겠는데 전 둘다 안편하니까 둘 중에

먼저 편해지는 방식을 채택하겠습니다.


😋제네릭에 제약 조건 주기

제네릭 함수에 어느정도 타입 힌트를 줄 수 있는 방법도 존재합니다.

만약 .length 프로퍼티정도만 허용해주고 싶은 경우엔

다음과 같이 정의를 할 수도 있습니다.

interface LengthWise {
  length: number;
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length);
  return text;
}

이렇게 작성하면 type을 강제하지는 않지만

length에 동작하는 인자만 넘겨받을 수 있게됩니다.

interface LengthWise {
  length: number;
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length);
  return text;
}


console.log(logText('123')) // okay
console.log(logText([1,2,3])) // okay
console.log(logText(123)) // error

length 프로퍼티를 가지고있는 애들만 들어갈 수 있게 되었네요!

function getProperty<T, O extends keyof T>(obj: T, key: O) {
  return obj[key];
}
let obj = { a: 1, b: 2, c: 3 };

getProperty(obj, 'a'); // okay
getProperty(obj, 'z'); // error: "z"는 "a", "b", "c" 속성에 해당하지 않습니다.

객체에서도 제약조건을 넣어줄 수 있습니다.

두번째 인자로 들어갈 key 매개변수는 obj의 내부에 존재하는 속성만 들어갈 수 있습니다.

따라서 'a'는 존재하는 속성이기 때문에 들어갈 수 있으나

'z'는 존재하지 않는 속성이기 때문에 error가 발생합니다.


🐶마치며

제네릭.. 타입스크립트에서 중요하다는 말만 들었는데

이렇게만 봐도 벌써 유용해보이네요

하지만 문법이 조금 헷갈려서 제대로 쓰는데에 시간이 좀 걸릴 것 같습니다.


이해했다고 생각할 때가 가장 무서울 때다.

 

반응형