게으른개발너D

Functions - call signature, overriding, polymorphism, generics 본문

개발/TypeScript

Functions - call signature, overriding, polymorphism, generics

lazyhysong 2023. 4. 4. 00:24

✨ Call Signatures ✨

 

arrow function 으로 typescript 함수를 작성해 보자

const add = (a: number, b: number) => a + b;

우리는 위에서처럼 타입을 적지않고 add 함수만의 타입을 만들고 싶다.

 

type Add = (a: number, b: number) => number;

const add:Add = (a, b) => a + b;

이게 바로 함수의 call signature 타입을 만드는 것이다.

 

이렇게 우리는 함수를 구현하기 전에 타입을 미리 정할 수 있다.

처음에 우리가 타입을 생각할 수 있도록 할 수 있다.

 

 

✨ Overloading ✨

 

우리는 대부분 다른 사람들이 만든 외부 라이브러리를 사용할텐데, 이런 패키지나 라이브러리들은 오버로딩을 엄청 많이 사용한다.

이전 게시글에 type signature을 다뤘었는데, 우리가 타입스크립트에게 이 함수가 어떻게 호출되는지 설명해주는 부분이었다.

type Add = (a: number, b: number) => number

이 방법은 signature을 만드는 가장 짧은 방법이다.

 

아래 이러한 방법도 가능하다.

type Add = {
  (a: number, b: number): number
}

이러한 방법이 존재하는 이유는 오버로딩 때문이다.

 

오버로딩은 함수가 여러개의 call signatures를 가지고 있을 때 발생시킨다.

그냥 여러개가 아니라 서로 다른 여러개의 call signature를 가졌을 때이다.

 

 

type Add = {
  (a: number, b: number): number
  (a: number, b: string): number
}

아주 나쁜 예이지만 이런식으로 Add 타입을 정의할 수 있다.

그러면 b는 number도 될 수 있고 string도 될 수 있다. (string | number)

그렇게 된다면 아래 함수는 에러 표시가 뜨게 된다.

const add:Add = (a, b) => a + b

모두 만족할 수 있도록 하려면 함수가 이런식으로 구성되면 될 것이다.

const add: Add = (a, b) => {
  if(typeof b === "string") return a;
  return a + b;
}

아주 나쁜 예시지만 오버로딩의 핵심을 알아볼 수 있다.

 


우리가 사용하는 library에는 몇가지 함수가 있다.

그리고 그 함수에는 string을 보낼 수 있거나, configuration같은 객체를 보낼 수 있도록 허용했을 것이다.

 

nextjs에서의 router를 예로 들어보자

Router.push("/home");

Router.push({
  path: "/home",
  state: 1,
});

/home으로 보내는 기능을 string으로도, object로도 쓸 수 있다.

 

비슷한 예로 오버로딩이 쓰이는 코드를 작성해 보자.

type Config = {
  path: string,
  state: object,
}

type Path = {
  (path: string): void
  (config: Config): void
}

const path: Path = (config) => {
  if(typeof config === "string") {
    console.log(config);
  } else {
    console.log(config.path);
  }
}

다른 오버로딩의 기능은 여러개의 argument들을 가지고 있을 때 나타나는 효과이다.

 

예를들어 이런 signature 타입들이 있을 수 있다.

type Add = {
  (a: number, b: number): number
  (a: number, b: number, c: number): number
}

다른 개수의 파라미터를 가지만 나머지 하나의 파라미터도 타입을 지정해 줘야 한다.

여기서 c 파라미터는 옵션같은 것이다. 따라서 함수는 이런식으로 적어줄 수 있다.

 

const add: Add = (a, b, c?: number) => {
  if(c) return a + b + c;
  return a + b;
};

 

 

 

✨ Polymorphism ✨

 

Polymorphism(다양성)은 '여러가지 다른 구조들'이란 뜻이다.

 

call signature를 만들자!

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

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

배열 요소를 받아서 그 배열의 요소를 각각 콘솔로 찍는 작업이다.

 

여기서 문제가 있다.

파라미터로 배열을 받을 수 있는데, boolean으로 받을 수도 있고, object로 받을 수도 있고 뭐든 배열로 받을 수 있다.

오버로딩을 해주자

 

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


superPrint([1, 2, 3, 4]);
suserPrint([true, false, false, true]);

이렇게 하면 number 배열과 boolean 배열을 아규먼트로 넣어서 함수를 사용할 수 있다.

하지만 string 배열을 넣으면 에러가 발생한다.

또, [1, 2, true, false] 이러한 배열을 넣어도 에러가 발생한다.

 

그러면 string 배열과 number 도는 boolean 배열을 받는 call signature을 또 넣어줘야할까?

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

이렇게 하면 너무 비효율적이다. 

그래서 그 대신, 우리는 generic을 사용할 것이다.

 

concrete type은 string, number, boolean, void, unknown 등등 이런 것들이다.

여기서 우리는 타입스크립트한테 generic type을 받을 거라고 알려줄 거다.

generic이란, type의 placeholder같은 것인데, concrete type을 사용하는 것 대신 쓸 수 있다.

우린 타입스크립트로 placeholder를 작성할 수 있고 타입스크립트는 그게 뭔지 추론해서 함수를 사용하는 것이다.

 

우리가 call signature를 작성하는데, concrete type을 알 수 없을 때도 있다. 그럴 때 generic을 사용한다.

 

generic을 사용하려면 먼저, 타입스크립트에 generic을 사용하고싶다고 알려줘야한다.

 

방법은 <> 이 괄호 안에 generic 이름을 넣어주는 것이다.

여러 패키지나 라이브러리를 쓸 텐데 가장 많이 보는 건 T 또는 V이다.

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

이렇게 해당 call signature에 generic을 쓰고싶다고 알렸다.

그리고 저기 number를 typePlaceholder로 바꿔줘야한다.

type SuperPrint = {
  <TypePlaceholder>(arr: TypePlaceholder[]): void
}
superPrint([1, 2, 3, 4]);
superPrint([true, false, false, true]);
superPrint(["a", "b", "c"]);
superPrint([1, 2, true, false]);

이렇게 바꾸면 첫번째에선 타입스크립트가 해당 라인에서 superPrint가 number 타입의 배열로 동작한다는 걸 알게된다.

마우스를 올려보면 보일 것이다. <number>(arr: number[]) => void

타입스크립트가 타입을 유추하고 그 유추한 타입으로 call signature를 우리에게 보여주는 것이다.

 

함수의 리턴 타입을 배열의 첫번째 요소를 리턴하는 걸로 바꿔보자.

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

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

 

이 superPrint 함수는 많은 형태를 가지고 있다. 이게 바로 Polymorphism이다.

 

 

 

✨ Generics ✨

 

generic은 우리가 요구한 대로 signature를 생성해 줄 수 있는 도구이다.

 

SuperPrint type의 generic을 하나 더 생성해주자.

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

 

타입스크립트는 제네릭을 처음 인식했을 때와 제네릭의 순서를 기반으로 제네릭의 타입을 알게 된다.

 

 

 

 

✨ Conclusions ✨

 

라이브러리를 만들거나 다른 개발자가 사용할 기능을 개발하는 경우엔 제네릭이 유용할 것이다.

SuperPrint를 함수로 바로 구현할 수 있다.

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

함수를 사용할 때 타입스크립트가 유추하는 것 말고 내가 직접 확실하게 정의해 줄 수도 있다.

const a = superPrint<boolean>([true, false, false, true])

제네릭을 사용해서 타입을 생성할 수도 있고 어떤 경우는 타입을 확장할 수 있다.

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

const hwayeon: Player<{favFood: string}> = {
  name: "Hwayeon",
  extraInfo: {
    favFood: "moosaengchae",
  }
}

이렇게 바꿔줄 수도 있다.

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

type HwayeonPlayer = Player<{favFood: string}>

const hwayeon: HwayeonPlayer = {
  name: "Hwayeon",
  extraInfo: {
    favFood: "moosaengchae",
  }
}

또는 이렇게 확장 시킬 수도 있다.

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

type HwayeonExtra = {
  favFood: string
}

type HwayeonPlayer = Player<HwayeonExtra>

const hwayeon: HwayeonPlayer = {
  name: "Hwayeon",
  extraInfo: {
    favFood: "moosaengchae",
  }
}

extraInfo가 null인 새로운 object를 생성해 보자

const lucky: Player<null> = {
  name: "lucky",
  extraInfo: null
}

제네릭을 많이 보게될 건데, 타입스크립트는 제네릭으로 많이 이루어져 있다.

예를들어 이런게 있다.

type A = Array<number>

let a:A = [1, 2, 3, 4]

 

 

파라미터로 number array를 받는 함수는 이렇게 두가지로 정의할 수 있다.

function printAllNumbers(arr: number[]) {

}

function printAllNumbers(arr: Array<number>) {

}

리액트에서는 이렇게 쓰였었다.ㅎㅎㅎㅎㅎ

useState<number>()

 

'개발 > TypeScript' 카테고리의 다른 글

window 객체에 property 추가하기  (0) 2023.08.23
Classes and Interfaces - classes, interfaces, polymorphism  (0) 2023.04.04
Overview of Typescript  (0) 2023.04.03
Software Requirement  (0) 2023.04.03
Comments