[노마드TS] day 4

·2023년 3월 9일
0

TypeScript공부중

목록 보기
5/6

#3.2 Polymorphism

다형성(polymorphism)
ts에 어떻게 다형성을 주는지 살펴보자. generic이라 불리는 것을.

다형성이란, 우선 poly는 many,several, much, multi를 뜻을 가지고있고,
morphos 혹은 morphism은 form, structure라는 뜻을 가지고 있다.
따라서 다형성이란 여러가지 다른 구조물 이라는 뜻이다.

함수는 기본적으로 여러가지 다른 모양 또는 다른 형태를 가지고있다.
앞서 공부했던 내용을 보면 함수는 다른 2~3개의 parameter를 가질 수 있다고 했었다. 또는 ts에서 함수는 string이나 object를 첫번째 파라미터로 가질 수 있다고 했다.

따라서 우리는 이미 약간의 여러가지 모양의 다형성(polymorphism)을 해본것이다.

이번에는 제네릭이 어떻게 좀 더 도움을 줄 수 있는지 알아보자.

타입에 신경쓰지 않고 숫자로 이루어져있는 배열을 받고, 그 배열의 요소인 숫자를 각각 하나씩 print(출력)해주는 함수를 예로 만들어보자.
또한 string으로 이루어져있는 배열을 받고, string을 하나씩 출력하자.

// 첫번째로 call signature를 선언.
type SuperPring = {
  (arr: number[]):void // number배열을 받고, void를 리턴한다. 왜냐면 함수는 아무것도 리턴하지 않으니까.
}

// 그다음에 superPrint함수를 만든다.
const superPrint: SuperPrint = (arr) => {
  arr.forEach(i=>console.log(i)); // 리턴하지 않고 각 요소를 출력한다.
}

위 예제에서 문제가 있다. 우리는 배열이 number로 이루어져있는지, boolean인지 string인지 정하지 않은 배열을 받아서 출력하려고한다.

// 첫번째로 call signature를 선언.
type SuperPring = {
  (arr: number[]):void
  (arr: boolean[]):void
}

// 그다음에 superPrint함수를 만든다.
const superPrint: SuperPrint = (arr) => {
  arr.forEach(i=>console.log(i)); 
}

superPrint([1,2,3,]); // 잘 동작
superPrint([true, false, true]); // 잘 동작
superPrint(['a','b','c']); // 동작 안 함. 에러 발생.

string배열을 받는 함수를 동작시키기위해서 call signature에 (arr: string[]):void를 추가해야할까?
물론 그렇게해도 동작은 되지만 정답은 아니다.

여기에서 다형성을 사용하여 ts에게 더 나은 방법으로 알려줄 수 있다.

type SuperPring = {
  (arr: number[]):void
  (arr: boolean[]):void
}

위 코드에서 number, booleanconcreate type이 아니다.
concreate type이란, 우리가 전부터 봐왔던 타입을 말한다.
number타입, boolean타입, string타입, void, unknown이 concreate type이다.

generic이란, 타입의 placeholder같은거다.
generic은 우리가 call signature를 작성할 때, 타입란에 들어올 확실한 타입을 모를 때 사용한다.

type SuperPring = {
  (arr: number[]):void
  (arr: boolean[]):void
  (arr: string[]):void
  (arr: number | boolean[]):void  
}

// 그다음에 superPrint함수를 만든다.
const superPrint: SuperPrint = (arr) => {
  arr.forEach(i=>console.log(i)); 
}

superPrint([1,2,3,]);
superPrint([true, false, true]);
superPrint(['a','b','c']);
superPrint([1,2,true,false]); // 를 받는 함수를 위해 call signature를 수정.

만약 위의 예시처럼 number와 boolean을 갖는 배열을 함수로 보낼때 call signature에 (arr: number | boolean[]):void를 추가해야 할까?
추가하면 물론 동작은 된다. 하지만 배열에 string이 추가되면? 그럼 또 call signature에 또 추가를 해야하고 무수한 가능성에 대해 생각해서 미리 작성해놔야한다.

이것은 옳은 방법이 아니다.

그래서 generic을 사용하는 것이다.
generic을 사용하는 방법은, 먼저 ts에 generic을 사용하고 싶다고 알려야한다.

type SuperPring = {
  <TypePlaceholder>(arr: TypePlaceholder[]):void
 }

위의 코드예시처럼 꺽쇠안에 generic을 입력 후 타입란에도 동일한 generic을 사용하면 된다.
대게 <TypePlaceholder> 라는 이름 대신 <T, V>를 많이 보게 될 것이다. 이름은 상관없다. 이름이 <Potato>가 될 수도 있는 것이다.

type SuperPring = {
   <TypePlaceholder>(arr: TypePlaceholder[]):void
}

// 그다음에 superPrint함수를 만든다.
const superPrint: SuperPrint = (arr) => {
  arr.forEach(i=>console.log(i)); 
}

superPrint([1,2,3,]); // 잘 동작
superPrint([true, false, true]); // 잘 동작
superPrint(['a','b','c']); // 잘 동작
superPrint([1,2,true,false]); // 잘 동작

그러면 아무 문제 없이 잘 동작하는걸 볼 수 있다.
ts가 타입추론을 하게 하는 것이다.
제네릭은 여전히 내가 함수에 타입을 입력하는 것을 허용한다.

다른 예를 들어보자.
만약 superPrint함수의 리턴 타입을 바꾸고 싶다면 함수는 배열을 받게되고 그 배열의 첫번째요소를 리턴하게 만들자.=== superPrint함수는 arr을 받고, 그 배열의 첫 번째 요소를 리턴하자.

type SuperPring = {
   <TypePlaceholder>(arr: TypePlaceholder[]) => TypePlaceholder
}

// 그다음에 superPrint함수를 만든다.
const superPrint: SuperPrint = (a) => a[0];

cosnt a = superPrint([1,2,3,]); // 타입 number
cosnt b = superPrint([true, false, true]); // 타입 boolean
cosnt c = superPrint(['a','b','c']); // 타입 string
cosnt d = superPrint([1,2,true,false,'a']); // 타입 string | number | boolean

리턴타입이 ts가 타입추론하여 나온다.

위의 call signature는 type SuperPrint = <T>(arr: T[]) => T로 자주 쓰인다.


#3.3 Generics Recap

제네릭은 내가 요구한대로 call signatuer를 생성해줄 수 ㅣ있는 도구다.

superPrint타입 제네릭을 하나 더 추가하고싶다고 예를 들어보자.
아주 쉽다.

type SuperPrint = <T, M>(arr: T[], b:M) => T

const superPrint: SuperPrint = (a) => a[0];

cosnt a = superPrint([1,2,3], 'x');

여기서 b부분이 ts가 알게되는 곳이다. ts는 제네릭이 처음 사용되는 지점을 기반으로 이 타입이 무엇인지 알게된다.
(, 'x') 두번째 인자를 넣음으로써 ts는 이제 두번째 arguments가 함수에서 제네릭으로 되었다는것을 알게된다.
ts는 제네릭을 처음 인식했을 때와 제네릭의 순서를 기반으로 제네릭의 타입을 알게된다.


#3.4 Conclusions

제네릭 마무리
앞으로 우리는 제네릭을 사용해서 직접 call signature를 만들 일은 없을 것이다.
라이브러리를 만드는 개발자는 유용하겠지만, 그 외 대부분의 경우에는 직접 제네릭을 작성할 일은 없을 거다.

다른 사용 방법을 보자. 왜냐하면 함수의 call signature 외에 이것을 또 어디서 쓸 수 있는지 아는것도 중요하기 때문이다.

위에서 들었던 예시는 이런식으로 모양을 변형할 수 있다. 위의 예시와 똑같은 동작을 한다.

function superPrint<T>(a: T[]){
  return a[0]
}

cosnt a = superPrint([1,2,3], 'x');

또는 제네릭을 이런식으로 사용할 수 있다.

type Player<E> ={
  name:string
  extraInfo:E
}

const coco: Player<{favFood:string}> = {
  name:'koko',
  extraInfo:{
    favFood:'kimchi'
  }
}

위의 예제는 이렇게 할 수도 있다.

type Player<E> ={
  name:string
  extraInfo:E
}

type CocoPlayer = Player<{favFood:string}> // new

const coco: CocoPlayer = { // new
  name:'koko',
  extraInfo:{
    favFood:'kimchi'
  }
}

이렇게도 가능하다

type Player<E> ={
  name:string
  extraInfo:E
}

type CocoExtra = { // new
  favFood:string
}

type CocoPlayer = Player<CocoExtra> // new

const coco: CocoPlayer = {
  name:'koko',
  extraInfo:{
    favFood:'kimchi'
  }
}

이런식으로 코드 확장이 가능하다.

다른 플레이어를 추가해보자

type Player<E> ={
  name:string
  extraInfo:E
}

type CocoExtra = { // new
  favFood:string
}

type CocoPlayer = Player<CocoExtra> // new

const coco: CocoPlayer = {
  name:'koko',
  extraInfo:{
    favFood:'kimchi'
  }
}

const vita: Player<null> = {
  name:'vitamin',
  extraInfo:null
}

type들까리 일종의 상속을 할 수 있다. 굳이 따지자면 지금은 상속이 아닌 재사용이긴하다.

제네릭은 함수에서만 쓰이는게 아니라 정말 많은 곳에서 쓰인다.
예를 들면, 대부분의 기본적인 ts타입은 제네릭으로 만들어져있다.


위 사진에서 자동완성의 ts표준 라이브러리를 보면 Array에 interface Array<T>라고 되어있는걸 볼 수 있다.

type A = Array<number>
let a:A = [1,2,3,4]

위 예시 또한 제네릭을 사용하는 다른 방법 중 하나이다.

만약, printAllNumber라는 함수를 만들고 여기에 number로 이루어진 배열을 받는다면 코드는 아래와 같을것이다.

function printAllNumber(arr:number[]){}
// 혹은 이렇게 쓸 수도 있다.
function printAllNumber(arr:Array<number>){}

Reactjs에서 useState는 제네릭을 받는다.

useState<number>()
profile
어두운 밤하늘, 밝은 달빛.

0개의 댓글