[TypeScript] FUNCTIONS #3.2 - #3.4

uxolrv·2022년 12월 15일
1

nomadCoder - TypeScript

목록 보기
5/9
post-thumbnail

📌 Polymorphism (다형성)

type SuperPrint = {
  (arr: number[]): void // 아무것도 return하지 않음
}

const superPrint: SuperPrint = (arr) => {
  arr.forEach(i => console.log(i))
}

number 배열을 받아서 배열의 요소를 각각의 콘솔로 찍는 함수

이때, boolean 타입의 배열이든, string 타입의 배열이든, 모든 배열을 인자로 받고 싶다면?


type SuperPrint = {
  (arr: number[]): void // 아무것도 return하지 않음
  (arr: boolean[]): void
  (arr: string[]): void
}

...

superPrint([1, 2, 3, 4])
superPrint([true, false, true])
superPrint(["a", "b", "c"])

이렇게 매번 call signature를 만들어도 되지만, 다형성을 활용한 더 좋은 방법이 있다.

⇒ ✨ generic type !








📌 generic type

타입의 placeholder 같은 개념으로, concrete 타입 대신 사용!


제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.

concrete type ? ex. number, boolean, string, unknown ...


실제 상황에서는 직접 generic을 사용하여 call signature를 생성하는 상황보다는 generic을 사용하는 경우가 더 많다. (라이브러리들이 generic을 통해 생성되기 때문에)



superPrint([1, 2, true, false]) // Err!

위 코드에서 에러가 발생하는 이유는 해당 인자들에 대한 call signature가 없기 때문이다.


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

이렇게 매번 call signature를 추가한다면, call signature를 모든 가능성을 다 조합해서 만들어야 한다.

어떤 타입의 배열이 들어오더라도, 작동되게 하려면 generic을 사용하면 된다!




💡 generic 사용법

TypeScript에게 generic을 사용하고 싶다고 알려야 한다.
<> 사용하여 정의!


type SuperPrint = {
  <T>(arr: T[]): void
}

해당 call signature가 argument로 generic 타입을 받음을 의미하는 코드

<>안의 이름은 자유지만, 보통 많은 라이브러리, 패키지 등에서 제네릭을 <T>, <V>로 표기한다.


// const superPrint: <boolean>(arr: boolean[]) => void
const superPrint: SuperPrint = (arr) => {
  arr.forEach(i => console.log(i))
}

superPrint([1, 2, 3, 4])
superPrint([true, false, true])
superPrint(["a", "b", "c"])
superPrint([1, 2, true, false])

✨ generic 타입의 call signature를 만들 경우,
TypeScript가 코드를 분석하여 타입을 유추하고, 유추한 타입으로 call signature를 대체해준다.

⇒ 모든 타입 케이스들을 일일이 작성하지 않아도 된다!



🔎 return 타입을 변경하고 싶다면?

type SuperPrint = {
  <T>(arr: T[]): T
}

const superPrint: SuperPrint = (arr) => arr[0]

superPrint가 무언가를 return하는 함수가 되었으므로, void(아무것도 return X)를 T로 변경하기만 하면 된다.


const a = superPrint([1, 2, 3, 4]) // const a: number

const b = superPrint([true, false, true]) // const b: boolean

const c = superPrint(["a", "b", "c"]) // const c: string

const d = superPrint([1, 2, true, false])  // const d: string | number | boolean

마찬가지로, TypeScript가 자동으로 return 타입을 유추한다!



🔎 왜 any 대신 generic을 쓸까?

type SuperPrint = (a: any[]): any

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

const a = superPrint([1, 2, 3, 4])
a.toUpperCase() // 에러 없음

any를 사용해도 타입 관련 에러는 나지 않는다.
그러나! any를 사용할 경우 타입 검사를 하지 않기 때문에, TypeScript의 보호장치에서 벗어나게 된다.

예시로 a.toUpperCase()를 실행할 경우, number 타입에 toUpperCase()를 하는 데도 어떠한 에러도 발생하지 않게 된다.

any는 말 그대로 모든 타입을 허용하는 타입이며, generic은 코드를 분석하여 알맞는 call signature를 생성하는 타입이다.



🔎 여러 개 generic를 사용하고 싶다면?

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

<> 안에 generic명을 추가한 후, generic을 사용하고자 하는 곳에 타입 표시를 해준다.








💡 결론 (Conclusions)

🔎 함수에 generic을 사용하는 또 다른 방식

// 이전 예시에서 사용한 방식 (타입을 따로 선언)
type SuperPrint = {
  <T>(arr: T[]): T
}

const superPrint: SuperPrint = (arr) => arr[0]


// 또 다른 방식
function superPrint<T>(a: T[]){
  return a[0]
}

위와 같이 작성하여도 동일하게 작동하게 된다.


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

const a = superPrint<boolean>([1, 2, 3, 4]) // Err!

선언한 함수를 호출할 때에는 <>로 타입을 명시할 수는 있으나, TypeScript가 타입을 추론하도록 하는 것이 가장 좋으므로 따로 타입을 작성하지 않는 것이 좋다!

현재 위 코드에서 에러가 나는 이유는 TypeScript에 overwrite(덮어쓰기)를 했기 때문이다. (인자의 generic이 boolean 타입인데, number 타입을 인자로 넘겨주었기 때문에)



🔎 generic을 통해 코드 확장하기

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

// 위 타입으로 함수를 구현할 때에는 Player의 extraInfo의 키와 타입을 필수적으로 작성해야 한다.
const nico: Player<{favFood: string}> = {
  name: "nico",
  extraInfo: {
    favFood: "kimchi"
  }
}

위 코드는 generic을 이용하여 아래와 같은 방식으로 확장이 가능하다.


// generic을 사용해 만들어진 타입, Player
type Player<E> = {
  name: string
  extraInfo: E
}

type NicoExtra = {
  favFood: string
}

// NicoExtra 타입을 Player 타입의 generic에 넣어 사용
type NicoPlayer = Player<NicoExtra>

const nico: NicoPlayer = {
  name: "nico",
  extraInfo: {
    favFood: "kimchi"
  }
}

이처럼 타입을 생성하고, 그 타입을 또 다른 타입에 넣어서 사용할 수 있다.



🔎 generic을 통해 타입 재사용성 높이기

const lynn: Player<null> = {
  name: "lynn"
}

extraInfo 값을 가지고 있지 않은 lynn이라는 변수를 선언

필수적이지 않은 extraInfo의 타입을 generic으로 사용하였기 때문에, 타입을 새로 만들지 않고 재사용할 수 있다.

✨ 이처럼 많은 요소들을 가지고 있는 하나의 타입이 있고, 그 안의 일부 요소 타입이 달라질 수 있다면 generic을 사용하여 타입 재사용성을 높일 수 있다!



🔎 어디에나 다양하게 사용되는 generic !

generic은 앞선 예시들처럼 함수에서만 쓰이는 것이 아니며,
대부분 기본적인 타입스크립트의 타입은 generic으로 만들어져 있다.


function printAllNumbers(arr: number[]) {}
function printAllNumbers(arr: Array<number>) {}

위 코드와 아래 코드(generic 형식으로 작성)는 동일하게 작동하는 코드!



🔎 React.js에서 사용되는 generic 예시

useState()

위와 같이 useState를 사용할 경우, TypeScript는 state의 타입을 알 수가 없다.
TypeScript에게 state의 타입을 알려주기 위해서는 아래와 같이 작성해야 한다.


useState<number>()

React.js의 함수인 useState를 사용할 때 generic을 사용하면, useState의 call signature가 해당 타입으로 생성된다.








profile
안녕하세연🙋 프론트엔드 개발자입니다

0개의 댓글