제네릭
타입스크립트를 이제 막 배우기 시작한 단계라면 제네릭에 대한 이해가 많이 어려울 것이다.
내 생각엔 타입스크립트를 써보고 익숙해지면서(함수 타입과 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 에 마우스를 올려보자.
제네릭 제약조건
앞쪽의 예제를 기억한다면 특정 타입들로만 동작하는 제네릭 함수를 만들고 싶을 수 있습니다.
앞서 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 |