________________________________________________________________________
TypeScript 에서 함수를 다뤄보자 -3-
< polymorphism 다형성 >
=>타입스크립트는 어떻게 다형성을 제공하는가. ‘제네릭(generic)’ 이라는 걸 통해서 제공한다고.
우선 용어부터 정의하고 가자.
“ 다형성 “ 이란 무엇인가.
그리스어로 ‘poly’ 란, many, several, mush, multi 를 뜻해. “Polygon” 흔히 우리가 다각형을 폴리곤이라 부르지?
즉 poly 는 많은, 다수라는 뜻.
다음으로, morphose 혹은 orphic 에 대해서도 알아보자.
해당 단어는 form, structure 즉 형태 또는 구조라는 뜻을 가지고 있어.
=>
Poly 와 morphose 를 조합하면 ‘여러 가지 다른 구조들’ 이라는 뜻이 돼.
타입스크립트에서 함수는 여러 가지 형태를 가질 수 있어.
당장 이전 챕터에서 살펴 보았듯이 다른 1~ 여러 개의 파라미터를 가질 수 있지.
또 해당 파라미터의 타입이 string 일 수도, number 일 수도, object 일 수도 있지.
즉 이미 우리는 앞서서 약간의 여러 가지 모양의 다형성을 경험해 본 거야.
이번 챕터에서는 ‘제네릭’ 이라는 개념이 어떻게 다형성에 조금 더 도움을 줄 수 있는지 알아보자.
1.
________________________________
< 제네릭의 기초에 대하여, concrete type에 대하여 >
이번에 우리가 해볼 활동은 간단해.
배열을 받고, 그 배열의 요소들을 하나씩 print 해줄 거야.
——
type SuperPrint = {
(arr:number[]) : void
}
const superPrint : SuperPrint = (arr) => {
arr.forEach(console.log)
}
——
=> 해당 구문에는 문제가 없지만, 우리에게는 문제가 있어.
매개변수인 배열에 number 뿐 아니라 다른 타입들도 넣고 싶은거지.
그렇다면, 매개변수로 다른 타입들도 넣어주고 싶다면 그 때마다 call signatures 를 추가해 줘야 되는 걸까?
——
type SuperPrint = {
(arr:number[]) : void
(arr:boolean[]) : void
(arr:string[]) : void
}
const superPrint : SuperPrint = (arr) => {
arr.forEach(console.log)
}
superPrint([1, 2, 3, 4]);
superPrint([false, true, false]);
superPrint(["1", "2", "3"]);
——
=> 이렇게? 정답은 “아니” 야.
물론 위처럼 작성해도 동작은 잘 하겠지.
허나 다형성을 활용하는 더 좋은 방법이 존재해.
타입스크립트에게 보다 더 나은 방법으로 이야기를 해줄 수 있어.
우선, signatures 인 (arr:number[]) : void 에서 “ number[] “ 와 같은 타입들은 “ concrete type “ 이 아니야.
< concrete type > 이란 무엇인가.
Concrete 타입은 number, boolean, string, void, unknown 이런 것들. 여태까지 우리가 봐 왔던 타입들.
우린 signatures 의 각 매개변수들이 ‘generic’ 타입을 받을 거라고 알려줄거야.
Generic 이란, 타입의 placeholder 같은 것.
Concrete 타입 대신에 generic 타입을 사용할 수 있어.
우리는 타입스크립트로 placeholder 를 작성할 것이고, 그것이 뭔지 추론해서 타입스크립트는 함수를 가동시키는 거지.
현재 call signatures 가 3개 있는데, 각 시그니쳐들은 파라미터로 숫자 배열, 불린 배열, 문자 배열을 받고 있어.
만약
——
superPrint([1, 2, true, false, “33”]);
——
이렇게 받고 싶다면? 배열 안에 어떤 타입이 섞여 있든지 간에 SuperPrint 타입이 잘 작동하게 하고 싶다면?
우리는 call signatures 를 작성할 때, 변수로 들어올 확실한 타입을 모를 때 generic 을 사용해.
결국에는 concrete 타입이 되겠지만, 미리 그 타입을 알 수는 없어.
=>
우리가 코드를 작성하고 함수를 사용할 때는 물론, concrete 타입을 사용해야 해. True, string, number 그런 것들.
허나 signatures 를 작성하는 과정에서 어떤 타입의 값이 들어올 지 예상할 수 없다면, 그 때 generic 을 사용하는 거야.
타입스크립트에 generic 을 사용하고 싶다고 알려주자.
——
type SuperPrint = {
<TypePlaceHolder>(arr:TypePlaceHolder[]) : void
}
——
=> 해당 signature 의 앞에 <> 를 붙이고, 그 안에 내가 원하는 이름으로 제네릭을 선언해 주면 돼.
이름은 뭐든 상관 없는데, 많은 패키지나 라이브러리에서 <T> 나 <V> 를 주로 보게 될거라고.
어쨌든 이렇게 <TypePlaceHolder> 라고 명시해 줌으로서, 타입스크립트는 해당 signature 가
제네릭을 받는 다는 걸 알게 되었어.
그리고 arr 라는 매개변수가 가지게 되는 타입을 “ TypePlaceHolder[] “ 라고 명시해 줘야 해.
그럼, 호출된 함수들 각각에 마우스를 올려서 어떤 signature 를 참고하고 있는지 확인해볼까?
——
type SuperPrint = {
<TypePlaceHolder>(arr:TypePlaceHolder[]) : void
}
const superPrint : SuperPrint = (arr) => {
arr.forEach(console.log)
}
superPrint([1, 2, 3, 4]); // const superPrint: <number>(arr: number[]) => void
superPrint([false, true, false]); // const superPrint: <boolean>(arr: boolean[]) => void
superPrint(["1", "2", "3"]); // const superPrint: <string>(arr: string[]) => void
superPrint([1, 2, true, false, '33']); // const superPrint: <string | number | boolean>(arr: (string | number | boolean)[]) => void
——
=> 제네릭 타입의 경우, 타입스크립트는 함수 호출 시 입력된 매개변수들을 보고 타입을 유추,
기본적으로 그 유추한 타입으로 call signature 를 개발자에게 보여줘.
즉, 제네릭 타입을 선언한 시점에서는 아직 타입을 확정짓지 않고 있다가, 실재로 매개변수로 값이 들어오면
그 값들의 타입을 유추해서 placeholder( 이 경우 제네릭 ) 대신 타입스크립트가 유추해서 발견한 타입으로 바꿔줘.
제네릭 덕분에 매 경우마다 일일이 call signature 를 명시해 줄 필요가 없어진 것.
위의 경우는 제네릭 타입으로 매개변수를 받아들인 뒤, 리턴 타입이 void, 즉 아무것도 리턴하지 않는 함수야.
만약 리턴값을 받고 싶다면?
——
type SuperPrint = {
<TypePlaceHolder>(arr:TypePlaceHolder[]) : TypePlaceHolder
}
const superPrint : SuperPrint = (arr) => arr[0]
const qw = superPrint([1, 2, 3, 4]); // const qw: number
const qwe = superPrint([false, true, false]); // const qwe: boolean
const qwer = superPrint(["1", "2", "3"]); // const qwer : string
const qwert = superPrint([1, 2, true, false, '33']); // const qwert : number
——
=> superPrint() 함수는 인자로 받은 배열의 첫 번째 요소를 리턴하고 있어.
<TypePlaceHolder>(arr:TypePlaceHolder[]) : TypePlaceHolder 에서 return 타입인 “ : TypePlaceHolder “ 이,
실제로 리턴되는 데이터의 타입에 따라 변화하여 적용되는 것을 확인할 수 있어.
이것이 바로 다형성.
제네릭 타입을 도입함으로서, type SuperPrint 는 <TypePlaceHolder>(arr:TypePlaceHolder[]) : TypePlaceHolder 한 줄로
각종 다양한 형태, 개수의 매개 변수를 모두 커버할 수 있게 된 거야.
덕분에 해당 타입을 signature 로 채택한 superPrint 함수는 각기 다른 형태와 개수의 매개 변수를 받아들이더라도 모두 문제없이 로직을 잘 수행하고 있어.
앞으로 우리는
<T>(arr: T[]) : T
이와 같은 코드들을 많은 패키지와 라이브러리에서 보게 될 거야.
사실 위에서 배운 제네릭 사용법이 가장 일반적인 사용법은 아니야. 실제로 react.js 를 사용하다 보면 위 와는 다른 형태의 제네릭 사용 구문을 만나게 될 거고.
그래도 위의 사용 방법이 여러 방법들 중 가장 어려운 축에 속하니, 걱정하지 말자고.
'내일배움캠프_개발일지 > TypeScript 연습' 카테고리의 다른 글
TypeScript 연습 <1> -8- (0) | 2023.01.30 |
---|---|
TypeScript 연습 <1> -7- (0) | 2023.01.30 |
TypeScript 연습 <1> -5- (0) | 2023.01.26 |
TypeScript 연습 <1> -4- (1) | 2023.01.25 |
TypeScript 연습 <1> -3- (0) | 2023.01.25 |