🤹🏻‍♀️ Javascript/🥎 Typescript

[TS] 제네릭

ji-hyun 2022. 5. 7. 17:37

제네릭

타입스크립트를 이제 막 배우기 시작한 단계라면 제네릭에 대한 이해가 많이 어려울 것이다.

내 생각엔 타입스크립트를 써보고 익숙해지면서(함수 타입과 interface extends 같은 것에 익숙해진 후) 나중에 제네릭을 배우면 좀 더 이해가 쉽지 않을까 싶다. (개인적인 추천)

 

 

 

 

 

제네릭이란

선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 '재사용'이 가능하다는 장점이 있다.

 

 

 

 

 

제네릭이 없다면...

 

 

위처럼 generic을 쓰지 않는다면,

1) 타입을 미리 지정하거나

2) any 를 이용하여 구현할 수 있다.

 

 

 

 

1) 타입을 미리 지정하자면, 확실한 타입체크가 이뤄질 수 있겠지만 항상 number라는 타입을 받아야하므로 범용성이 떨어진다.
2) 그렇다고 any를 사용한다면 자료의 타입을 제한할 수 없을 뿐더러, 이 function을 통해 어떤 타입의 데이터가 리턴되는지 알 수 없다. (= number 를 넘겨줘도 any 타입이 반환된다는 정보를 얻는다)

 

 

 

 

 

 

 

 

 

 

<T>(arg: T):T 

 

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

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

 

여기에서는 T 를 반환 타입으로 다시 사용합니다.

인수와 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있습니다. 이를 통해 타입 정보를 함수의 한쪽에서 다른 한쪽으로 운반할 수 있게끔 합니다.

 

 

 

이 버전의 logText 함수는 타입을 불문하고 동작하므로 제네릭이라 할 수 있습니다. 

any 를 쓰는 것과는 다르게 인수와 반환 타입에 number 를 사용한 위의 첫 번째 identity 함수만큼 정확합니다.

(즉, 어떤 정보도 잃지 않습니다)

 

 

 

 

 

 

 

 

일단 제네릭 logText 함수를 작성하고 나면, 두 가지 방법 중 하나로 호출할 수 있습니다.

1. 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법

 

let output = logText<string>("myString"); // 출력 타입은 'string'입니다.
// let output: string

 

여기서 우리는 함수를 호출할 때의 인수 중 하나로써 Type 를 string 으로 명시해 주고 인수 주변에 ( ) 대신 < > 로 감싸주었습니다.

 

 

 

 

 

2. 가장 일반적인 방법 - 타입 인수 추론을 사용.

즉 우리가 전달하는 인수에 따라서 컴파일러가 Type 의 값을 자동으로 정하게 하는 것

 

let output = identity("myString"); // 출력 타입은 'string'입니다.   
// let output: string

 

타입 인수를 꺾쇠괄호(<>)에 담아 명시적으로 전달해 주지 않은 것을 주목하세요;

 

컴파일러는 값인 "myString"를 보고 그것의 타입으로 Type를 정합니다. 인수 추론은 코드를 간결하고 가독성 있게 하는 데 있어 유용하지만 더 복잡한 예제에서 컴파일러가 타입을 유추할 수 없는 경우엔 명시적인 타입 인수 전달이 필요할 수도 있습니다.

 

 

 

 

 

 

 

제네릭 타입 변수 작업

제네릭을 사용하기 시작하면, identity 와 같은 제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요합니다. 즉, 이 매개변수들은 실제로 any 나 모든 타입이 될 수 있는 것처럼 취급할 수 있게 됩니다.

 

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

 

 

 

 

 

함수 호출 시마다 인수 arg 의 길이를 로그에 찍으려면 어떻게 해야 합니까? 아마 이것을 쓰고 싶을 겁니다:

 

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // 오류: Type에는 .length 가 없습니다.
// Property 'length' does not exist on type 'Type'.
  return arg;
}

 

 

 

이렇게 하면, 컴파일러는 arg의 멤버 .length 를 사용하고 있다는 오류를 낼 것입니다만, 어떤 곳에서도 arg 가 이 멤버가 있다는 것이 명시되어 있지 않습니다. 이전에 이러한 변수 타입은 any나 모든 타입을 의미한다고 했던 것을 기억하십시오. 따라서 이 함수를 쓰고 있는 사용자는 .length 멤버가 없는 number를 대신 전달할 수도 있습니다

 

 

실제로 이 함수가 Type가 아닌 Type의 배열에서 동작하도록 의도했다고 가정해봅시다. 배열로 사용하기 때문에 .length 멤버는 사용 가능합니다. 다른 타입들의 배열을 만드는 것처럼 표현할 수 있습니다.

 

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length); // 배열은 .length를 가지고 있습니다. 따라서 오류는 없습니다.
  return arg;
}

 

 

 

 

 

 

 

 

제네릭 타입

이전 섹션에서 우리는 타입을 초월한 제네릭 함수 identity 를 만들었습니다. 이번 섹션에서는 함수 자체의 타입과 제네릭 인터페이스를 만드는 방법에 대해 살펴보겠습니다.

제네릭 함수의 타입은 함수 선언과 유사하게 타입 매개변수가 먼저 나열되는, 비-제네릭 함수의 타입과 비슷합니다.

 

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

let myIdentity: <T>(arg: T) => T = identity;

 

 

 

 

또한 타입 변수의 수와 타입 변수가 사용되는 방식에 따라 타입의 제네릭 타입 매개변수에 다른 이름을 사용할 수도 있습니다.

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

let myIdentity: <U>(arg: U) => U = identity;

 

 

 

 

 

 

제네릭 타입을 객체 리터럴 타입의 함수 호출 시그니처로 작성할 수도 있습니다:

 

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

let myIdentity: { <T>(arg: T): T } = identity;

 

 

 

 

 

이것들로 첫 번째 제네릭 인터페이스를 작성할 수 있습니다. 앞서 예제의 객체 리터럴을 인터페이스로 가져옵니다:

 

interface Cake {
  <T>(arg: T): T;
}

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

const mylog: Cake = log;

 

mylog 에 마우스를 올려보자.

const mylog: Cake

 

 

 

 

비슷한 예제에서, 제네릭 매개변수를 전체 인터페이스의 매개변수로 옮기고 싶을지도 모릅니다.

이를 통해 제네릭 타입을 확인할 수 있습니다 (예 - Dictionary 가 아닌 Dictionary<string>).

이렇게 하면 인터페이스의 다른 모든 멤버가 타입 매개변수를 볼 수 있습니다.

 

interface Cake<T> {
  (arg: T): T;
}

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

const mylog: Cake<string> = log;

 

mylog 에 마우스를 올려보자.

const mylog: Cake<string>
 

 

 

 

 

제네릭 제약조건

앞쪽의 예제를 기억한다면 특정 타입들로만 동작하는 제네릭 함수를 만들고 싶을 수 있습니다.

앞서 loggingIdentity 예제에서 arg의 프로퍼티 .length에 접근하기를 원했지만, 컴파일러는 모든 타입에서 .length 프로퍼티를 가짐을 증명할 수 없으므로 경고합니다.

 

 

any와 모든 타입에서 동작하는 대신에, .length 프로퍼티가 있는 any와 모든 타입들에서 작동하는 것으로 제한하고 싶습니다. 타입이 이 멤버가 있는 한 허용하지만, 최소한 .length 가 있어야 합니다. 그렇게 하려면 Type 가 무엇이 될 수 있는지에 대한 제약 조건을 나열해야 합니다.

 

 

 

이를 위해 우리의 제약조건이 명시하는 인터페이스를 만듭니다. 여기 하나의 프로퍼티 .length 를 가진 인터페이스를 생성하였고, 우리의 제약사항을 extends 키워드로 표현한 인터페이스를 이용해 명시합니다:

 

interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

 

제네릭 함수는 이제 제한되어 있기 때문에 모든 타입에 대해서는 동작하지 않습니다.

 

 

 

interface Length {
  length: number;
}

function logging<T extends Length>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logging(2);
// logging 에 마우스를 올려보면 function logging<Length>(arg: Length): Length

 

2 를 집어넣으면 에러가 뜬다.

 

 

 

 

 

 

 

 

 

 

'🤹🏻‍♀️ Javascript > 🥎 Typescript' 카테고리의 다른 글

[TS] 조건부 타입  (0) 2022.05.07
타입 추론, 타입 단언  (0) 2022.03.06
타입스크립트 에러  (0) 2022.02.25
체크박스 typescript (useState Type)  (0) 2022.02.25
material ui - Pagination  (0) 2022.02.25