function logText(text: string): string {
return text;
}
이 함수의 인자와 반환 값은 모두 string으로 지정되어 있지만 만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any를 사용할 수 있습니다.
function logText(text: any): any {
return text;
}
이렇게 타입을 바꾼다고 해서 함수의 동작에 문제가 생기진 않습니다.
다만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없습니다.
왜냐하면 any라는 타입은 타입 검사를 하지 않기 때문입니다.
이러한 문제점을 해결할 수 있는 것이 제네릭입니다. 아래 코드를 보겠습니다.
function logText<T>(text: T): T {
return text;
}
먼저 함수의 이름 바로 뒤에 라는 코드를 추가했습니다. 그리고 함수의 인자와 반환 값에 모두 T 라는 타입을 추가합니다.
이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 됩니다.
따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 됩니다.
그리고 이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있습니다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
보통 두 번째 방법이 코드도 더 짧고 가독성이 좋기 때문에 흔하게 사용됩니다. 그렇지만 만약 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하면 됩니다.
앞에서 배운 내용으로 제네릭을 사용하기 시작하면 컴파일러에서 인자에 타입을 넣어달라는 경고를 보게 됩니다.
조금 전에 살펴본 코드를 다시 보겠습니다.
function logText<T>(text: T): T {
return text;
}
만약 여기서
아마 아래와 같이 코드를 작성할 겁니다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
위 코드를 변환하려고 하면 컴파일러에서 에러를 발생시킵니다.
왜냐하면 text에 .length가 있다는 단서는 어디에도 없기 때문이죠.
다시 위 제네릭 코드의 의미를 살펴보면 함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있습니다.
따라서, 함수의 인자와 반환 값 타입에 마치 any를 지정한 것과 같은 동작을 한다는 것을 알 수 있죠. 그래서 설령 인자에 number 타입을 넘기더라도 에러가 나진 않습니다.
이러한 특성 때문에 현재 인자인 text에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서 .length를 허용할 순 없습니다. 왜냐하면 number가 들어왔을 때는 .length 코드가 유효하지 않으니까요.
그래서 이런 경우에는 아래와 같이 제네릭에 타입을 줄 수가 있습니다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용합니다.
return text;
}
위 코드가 기존의 제네릭 코드와 다른 점은 인자의 T[] 부분입니다. 이 제네릭 함수 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받습니다. 예를 들면, 함수에 [1,2,3]처럼 숫자로 이뤄진 배열을 받으면 반환 값으로 number를 돌려주는 것이죠. 이런 방식으로 제네릭을 사용하면 꽤 유연한 방식으로 함수의 타입을 정의해줄 수 있습니다.
혹은 다음과 같이 좀 더 명시적으로 제네릭 타입을 선언할 수 있습니다.
function logText<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
앞에서 제네릭 타입 변수에서 살펴본 내용 말고도 제네릭 함수에 어느 정도 타입 힌트를 줄 수 있는 방법이 있습니다. 잠시 이전 코드를 보겠습니다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 납니다. 이럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성합니다.
nterface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
위와 같이 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 됩니다.
logText(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' }); // `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
내가 원했던 것은 제너릭으로 호출시 타입을 명시해 주어 T 타입으로 받아도 내부에서 내가 주는 타입의 속성에 접근하고 있다는 코드 힌트를 얻고 싶고 싶었는데 뭔가 단순히 여러가지 타입을 사용하고 있는 함수 라는 식으로 명시만 시켜주고 그 내부에서 좀 더 명확히 다시 타입을 정의해서 받아야 하는 그런 느낌이로구만????