목차
2. 제네릭의 기본 문법
3. 제네릭의 동작 원리
4. 제네릭 타입 변수
5. 제네릭 인터페이스
6. 제네릭의 타입 제한 - 정의된 타입 이용하기
7. 제네릭의 타입 제한 - keyof로 객체의 속성 제약하기
8. 제네릭을 사용하는 이유
타입스크립트의 제네릭에 대해서 알아보자!
1. "제네릭"이란?
* 제네릭: 타입을 미리 지정하지 않고 사용 시점에 구체적인 타입을 지정할 수 있게 해주는 타입스크립트의 기능
2. 제네릭의 기본 문법
function getText<T>(text: T): T {
return text;
}
- 함수의 이름 바로 뒤에 <T> 라는 코드를 추가한다. → <T>는 함수가 제네릭이며 타입 매개변수 T를 받을 수 있음을 나타낸다.
- 함수의 인자와 반환 값에 모두 T 라는 타입을 추가한다. → T 타입의 매개변수를 받고 T 타입의 값을 반환하는 함수 타입임을 나타낸다.
이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
보통 두 번째 방법이 코드도 더 짧고 가독성이 좋기 때문에 흔하게 사용된다.
그렇지만 만약 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하면 된다.
함수를 호출 할때는 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다.
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
3. 제네릭의 동작 원리
function getText<T>(text: T): T {
return text;
}
getText<string>();
이렇게 getText() 함수를 호출하면서 <string> 타입을 제네릭(함수에서 사용할 타입) 값으로 넘겨주면,
아래와 같이 getText<string> 이렇게 정의가 되는 것과 같다.
function getText<string>(text: T): T {
return text;
}
그리고 나서 함수의 인자로 hi 라는 값을 아래와 같이 넘기게 되면 아래와 같이 타입을 정의한 것과 같다.
function getText<string>(text: string): string {
return text;
}
getText<string>('hi');
위 함수는 입력 값의 타입이 string이면서 반환 값 타입도 string이어야 한다.
4. 제네릭 타입 변수
함수의 인자로 받은 값의 length를 확인하고 싶어서 이렇게 코드를 작성하면 에러가 발생한다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
왜냐하면 text에 .length가 있다는 단서는 어디에도 없기 때문이다.
만약 인자인 text에 number가 들어온다면 .length 코드가 유효하지 않게 된다. (number에는 length가 없어!)
따라서 컴파일러는 text에 number로 들어올 수도 있는 가능성 때문에, 문자열이나 배열이 들어와도 .length를 허용하지 않고 에러를 발생시키는 것이다.
그래서 이런 경우에는 아래와 같이 제네릭에 타입을 줄 수가 있다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용한다.
return text;
}
이 제네릭 함수 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받는다. → T[ ]
예를 들면, 함수에 인자로 [1,2,3]처럼 숫자로 이뤄진 배열을 받으면 반환 값으로 legth를 계산해서 number를 돌려준다.
이런 방식으로 제네릭을 사용하면 꽤 유연한 방식으로 함수의 타입을 정의해줄 수 있다.
혹은 다음과 같이 좀 더 명시적으로 제네릭 타입을 선언할 수도 있다.
function logText<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
다른 예시
function logTextLength<T>(text: T[]): T[] {
console.log(text.length);
text.forEach(function (text) {
console.log(text);
});
return text;
}
logTextLength<string>(["hi", "abc"]);
5. 제네릭 인터페이스
우선 제네릭 표기 방식을 숙지해야한다.
아래의 두 코드는 같은 의미이다.
function logText<T>(text: T): T {
return text;
}
// #1 : 화살표 함수 문법
let str: <T>(text: T) => T = logText;
// #2 : 객체 타입 리터럴 문법
// {} 안에 있는 함수가 호출 가능한 시그니처인 것을 나타내기 위해 {}를 사용
let str: {<T>(text: T): T} = logText;
// (참고: 위의 코드를 타입스크립트를 쓰지 않고 쓰면 아래와 같다.
function logText(text) {
return text;
}
let str = logText;
위의 코드는 타입스크립트에서 제네릭 함수 logText를 사용하는 방법을 보여준다.
1. logText 함수는 제네릭 타입 T를 받아들이는 함수로, 받은 text를 그대로 반환한다.
2. let str: <T>(text: T) => T = logText;은 str이라는 변수를 선언하는 부분이다.
logText 함수를 str 변수에 할당하고 있다.
<T>(text: T) => T 는 제네릭 타입 T를 받는 함수 타입을 나타낸다.
따라서, str 변수는 logText 함수와 동일한 함수 형태를 가지며, 제네릭 함수를 가리키게 된다. 이렇게 하면 str을 호출하면 내부적으로 logText 함수가 실행되는 것과 같다.
위와 같은 변형 방식으로 제네릭 인터페이스 코드를 다음과 같이 작성할 수 있다.
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;
이와 같은 방식으로 제네릭 인터페이스 뿐만 아니라 클래스도 생성할 수 있다.
다만, 이넘(enum)과 네임스페이스(namespace)는 제네릭으로 생성할 수 없다.
6. 제네릭의 타입 제한 - 정의된 타입 이용하기
제네릭 타입 변수에서 살펴본 내용 말고도 제네릭 함수에 어느 정도 타입 힌트를 줄 수 있는 방법이 있다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 난다.
럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성한다.
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` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
위와 같이 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.
7. 제네릭의 타입 제한 - keyof로 객체의 속성 제약하기
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption; // ShoppingItem에 있는 key값들 중에 한가지가 바로 제네릭이 된다.
}
getShoppingItemOption("name") // okay
getShoppingItemOption("price") // okay
getShoppingItemOption("stock") // okay
getShoppingItemOption("a") // error : "a" 형식의 인수는 'keyof ShoppingItem' 형식의 매개 변수에 할당될 수 없습니다
8. 제네릭을 사용하는 이유
1) 타입 검증 가능
- 제네릭을 사용하면, 함수를 호출할 때 타입스크립트가 함수의 '입력 값 타입'과 '출력 값 타입'이 동일한지 검증할 수 있게 된다.
- any를 사용했을 때는 타입 검사를 하지 않아서 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없는데, 제네릭을 사용하면 이러한 문제점을 해결할 수 있다.
2) 코드의 재사용성과 유연성
- 제네릭은 타입이나 인터페이스를 정의할 때 특정한 타입을 지정하지 않고, 이후 사용될 때 동적으로 타입을 지정할 수 있도록 한다.
3) 코드 중복 방지
- 제네릭을 사용하면, 여러 종류의 타입에 대해 동일한 코드를 사용할 수 있어서 코드 중복을 줄일 수 있다.
- 예를 들어, 함수나 클래스를 정의할 때 제네릭을 사용하여 해당 함수나 클래스를 다양한 타입에 대해 사용할 수 있도록 만들 수 있다.
4) 타입 안정성을 확보
- 제네릭을 이용하면 컴파일 시점에 타입 안전성을 확보할 수 있다. 이는 코드의 신뢰성을 높이고 디버깅 과정에서 발생할 수 있는 에러를 줄여준다.
- 예를 들어, 배열이나 리스트와 같은 컬렉션을 다룰 때 제네릭을 사용하여 컬렉션의 요소 타입을 동적으로 지정할 수 있다. 이를 통해 잘못된 타입의 요소가 컬렉션에 추가되는 것을 방지할 수 있다.
'Languages > TypeScript' 카테고리의 다른 글
[TypeScript] 타입 단언 (Type Assertion) (0) | 2024.09.30 |
---|---|
[TypeScript] 타입 건전성 (Soundness ) (0) | 2024.09.29 |
[TypeScript] 이넘 (Enums) (0) | 2024.02.29 |
[TypeScript] 연산자를 이용한 타입 정의 (Union Type, Intersection Type) (0) | 2024.02.24 |
[TypeScript] 타입스크립트의 인터페이스(interface) (0) | 2024.02.24 |