[Type Script] 함수: 콜 시그니처, 오버로딩, 폴리모피즘(다형성), 제네릭

summereuna🐥·2023년 8월 24일
0

TypeScript

목록 보기
7/13

함수에 적용할 수 있는 타입에 대해 알아보자.

Call Signatures


객체만의 타입을 만들고 싶을 때, 이렇게 별칭 타입을 만들어서 객체만의 타입을 지정할 수 있다.

type Player = {
  name: string,
  age: number
}

const rm: Player = {
  name: "rm",
  age: 28
}

함수만의 타입을 만들고 싶다면 어떻게 하면 될까?

함수만의 타입 만들기: 콜 시그니처

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

콜 시그니처함수 위에 마우스를 올렸을 때 보게 되는 것을 말한다.

  • (a: number, b: number) => number 이 부분이 콜 시그니처이다.

  • 콜 시그니처는 타입스크립트에게 이 함수가 어떻게 호출되는지 설명해주는 부분이다.
    파라미터의 타입이 무엇인지, 그리고 함수의 반환 타입이 무엇인지 말이다.
    함수가 어떻게 구현되는지 알려주는 것이 아니다. 함수 인자(arguments)의 타입함수의 반환 값이 가지는 타입을 알려준다.

나만의 콜 시그니처 선언하는 방법

//나만의 함수 콜 시그니처 선언
type Add = (a: number, b:number) => number;

//함수 타입에 콜 시그니처 명시
const add: Add = (a, b) => a+b;

이렇게 하면 직접 add 함수의 인자에 타입을 명시할 필요가 없어지기 때문에, 타입 명시하는 코드를 분리해서 구현할 수 있다.

  • 프로그램을 디자인하면서 타입을 먼저 생각하고
  • 그러고 나서 코드 구현

리액트로 props로 함수를 보낼 때 타입스크립트에게 함수가 어떻게 작동하는지 설명해줘야 하는데, 그럴 때 많이 사용한다.


Overloading(오버로딩)


function overloading이나 method overloading이라고도 부른다.

실제로 많은 오버로딩된 함수를 직접 작성하지는 않는다. 그대신 대부분 다른 사름들이 만들어 둔 외부 라이브러리를 사용하는데, 이런 패키지나 라이브러리들은 오버로딩을 엄청 많이 사용한다.

그래서 오버로딩이 어떻게 생겼는지 알아두자 ^~^!

콜 시그니처를 작성하는 두 번째 방법

//나만의 함수 콜 시그니처 선언
type Add = (a: number, b:number) => number;
//(a: number, a:number) => number;
//1. 이는 단축키 같은 것으로, 콜 시그니처를 만드는 가장 간단하고 빠른 지름길이다.


//2. 콜 시그니처를 좀 더 길게 작성할 수도 있다.
type Add = {
  (a: number, b: number) : number;
}

콜 시그니처를 좀 더 길게 작성하는 두 번째 방법이 존재하는 이유는 오버로딩 때문이다.

1. 오버로딩 발생: 서로 다른 여러 개의 콜 시그니처를 가질 때

  • 오버로딩은 함수가 여러개의 콜 시그니처를 가지고 있을 때 발생한다.
  • 그냥 여러 개가 아니라 서로 다른 여러 개의 콜 시그니처를 가졌을 때 오버로딩이 발생한다.
//나만의 함수 콜 시그니처 여러개 선언
type Add = {
  (a: number, b: number) : number;
  (a: number, b: string) : number;
}

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

add 함수는 Add 타입을 가지는데, 이 Add 타입은

  • (a: number, b: number) : number; 이 모양으로 부를 수도 있고

  • (a: number, b: string) : number; 이 모양으로 불르 수도 있게 된다.

  • 그러면 즉시 타입스크립트는 잘못되었다는 것을 알게 된다.

    왜냐하면 a는 항상 number이기 때문에 number + string은 있을 수 없는 일이기 때문이다.

  • b가 string도 될 수 있고, number도 될 수 있기 때문에 string과 number는 더할 수 없다고 오류가 뜬다.

따라서 타입을 체크해줘야 한다.

//나만의 함수 콜 시그니처 여러개 선언
type Add = {
  (a: number, b: number) : number;
  (a: number, b: string) : number;
}

const add: Add = (a, b) => {
  if (typeof b === "string") return a; //b가 string이면 a만 리턴
  return a+b; //b가 number면 a+b 리턴
}
//이제 오류 안뜸

별로 좋은 예시는 아니다. 왜냐하면 매우 소수의 함수만 이런식으로 작동하기 때문이다.
하지만 오버로딩의 핵심을 보여주는 좋은 예시였음!

  • 오버로딩은 여러 콜 시그니처가 있는 함수이다.

실제로 겪을 만한 예시

일부 라이브러리에는 몇 가가지 함수가 있는데, string을 보내거나 configuration 객체를 보낼 수 있게 허용하는 함수가 있을 수 있다.

예를 들면, NextJS에는 라우터를 가지는데, 페이지를 바꾸고 싶을 때 이런식으로 사용한다.

//1. string 값으로 "/home"을 주면 홈으로 이동
Router.push("/home");

//2. 갹체 안에 path를 설정하여 홈으로 이동하게 할 수도 있음
Router.push({
  path: "/home",
  state: 1, //추가로 다른 것도 넣을 수 있음
});

완벽한 오버로딩의 예시이다.

이럴 때 타입을 아래 처럼 만들면 된다.

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

//오버로딩: 콜 시그니처 여러 개 사용
type Push = {
  (path: string):void //path 인자로 string를 받고 아무것도 리턴하지 않는 함수
  (config: Config):void //config 인자로 객체를 받고 아무것도 리턴하지 않는 함수
} 

//오버로딩 사용하기
const push: Push = (config) => {
  
  //config 인자로는 string이나 Config 객체 타입을 받을 수 있으므로 체크해야 함
  if(typeof config === "string"){
    console.log(config); //이 스코프에서는 config가 스트링인게 확실
    
  } else {
    console.log(config.path);// 이 스코프에서는 config가 Config객체임이 확실
  }
}
  • config 인자로는 string이나 Config 객체 타입을 받을 수 있으므로 체크해야 함
  • 핵심은 string이나 Config 타입을 가지고 있다면 타입스크립트는 내부에서 그 타입을 체크하도록 해준다.

패키지나 라이브러리를 디자인할 때, 많은 사람들이 이런 식으로 오버로딩을 사용한다.


위의 예시는 인자의 개수는 같고 타입이 달랐다.

2. 오버로딩 발생: 파라미터 개수가 다른 콜 시그니처를 여러 개 가질 때

다른 여러 개의 argument를 가지고 있을때 발생하는 일을 알아보자.
예를 들어 하나의 콜 시그니처는 2개의 파라미터를 가지는데, 다른 콜 시그니처는 파라미터를 3개를 가진다고 해보자.

오류

//파라미터 개수가 다른 콜 시그니처를 여러개 가질 때
type Add = {
  (a: number, b: number) : number;
  (a: number, b: number, c: number ) : number;
}

const add: Add = (a, b, c) => {
  return a + b
}
//모든 콜 시그니처에 c가 있는게 아니기 때문에 오류 뜸!
  • 따라서 다른 개수의 파라미터를 가지게 되면, 나머지 파라미터도 타입을 지정해 줘야 한다.

정상 작동하려면 c가 "옵션"이고 아마 number타입일 거라고 알려주면 된다.

//파라미터 개수가 다른 콜 시그니처를 여러개 가질 때
type Add = {
  (a: number, b: number) : number;
  (a: number, b: number, c: number ) : number;
}

//Add를 부를 때, a와 b를 부르거나, 또는 abc를 부를 수 있도록 c는 옵셔널 하게 하면 된다.
//따라서 c는 아마도 number 일 것이라고 알려주면 된다.
const add: Add = (a, b, c?:number) => {
  if(c) return a + b + c //c가 있다면 a+b+c
  return a + b //c가 없다면 a+b
}
//그러면 오류 안뜸!

add(1,2); //3
add(1,2,3); //6

이런 유형은 그렇게 자주 보는 유형은 아닐 거라고 한다.


Polymorphism(다형성)


  • 그리스어로 poly는 many, several, much, multi의 뜻을 가지고 있다.
  • 그리스어로 morphos 혹은 morphic은 form(형태), stuructre(구조)라는 뜻을 가진다.

따라서 둘을 합치면, many structure(여러가지 다른 많은 구조, 모양들)가 된다.

기본적으로 함수는 여러가지 다른 모양 혹은 형태를 가지고 있다.

  • 위의 오버로딩의 예시에서 처럼, 타입스크립트에서 함수는 하나의 파라미터에 다른 타입을 가질 수 있거나, 다른 두 세개의 파라미터를 가질 수 있었다.
  • 이는 이미 모양의 다형성의 예시라고 할 수 있다.

Generics


숫자로 이루어진 배열을 받고, 그 배열의 요소를 하나씩 print해주는 함수를 만들어보자.

//number로 이루어진 배열 받아서 하나씩 출력
//string으로 이루어진 배열 받아서 하나씩 출력
//boolean으로 이루어진 배열 받아서 하나씩 출력

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

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

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

어떤 타입이든 작동할 수 있는 함수를 만들고 싶을 때, 타입을 설정해야 하는데, 이런식으로 하나씩 콜 시그니처를 늘리면서 타입을 일일히 설정해야 할까?

위의 콜 시그니처에 작성한 타입은 모두 concrete type이다.

  • concrete type에는 string, number, boolean, void, unknown 등이 있다.

직접 일일히 콜 시그니처를 만드는 대신, generic 타입 사용하기

이렇게 계속 콜 시그니처를 늘리는 대신, 타입스크립트에게 generic 타입을 받을 거라고 알려주면 좀 더 유연하게 어떤 타입이든 받을 수 있고 그 받은 타입에 따라 타입을 추론 할 수 있다.

  • generic이란 타입의 placeholder 같은 거다.
  • 타입스크립트에 일종의 플레이스홀더를 만들어서 그게 뭔지 추론해서 함수를 사용하게 하는거다.
  • 콜 시그니처를 작성 시, 여기 들어올 확실한 타입을 모를 때 generic을 사용한다. 결국엔 콘크리트 타입이 되겠지만, 그 타입을 미리 알 수 없을 때 사용 할 수 있다.
type SuperPrint = {
  <T>(arr: T[]) : void
    }
  • argument가 제네릭을 받는 다고 TS에게 알려줘야 한다.
    이렇게 <T> 꺽쇠에 식별자를 넣어서 이 콜 시그니처가 제네릭을 받는 다고 알려줄 수 있다.

  • 그리고 그 식별자T를 사용하고자 하는 타입(T 타입을 가진 배열, T[])에 넣어주면 된다.

그러면 이런 식으로 각각의 타입에 대해 추론하여 잘 작동한다.

  1. 인자로 number 타입이 전달되었을 때: number 타입으로 추론

  2. 인자로 string 타입이 전달되었을 때: string 타입으로 추론

  3. 인자로 boolean 타입이 전달되었을 때: boolean 타입으로 추론

  4. 여러 타입을 한꺼번에 인자로 전달하면: 그에 따라 타입을 유추하므로 여러 타입으로 추론

  • 이렇게 타입스크립트는 인자로 전달된 값을 보고 타입을 유추하고, 기본적으로 유추한 타입으로 call signature를 보여준다.

  • 이것이 바로 제네릭의 핵심이다.

리턴 값이 있는 경우

인자에 제네릭 타입이 든 배열을 보내면, 리턴 값으로 void 대신, 제네릭 타입이 하나 나오게 설정해 보자.

배열의 첫 번째 요소 반환하는 함수를 만들어보자.

type SuperPrint = {
    <T>(arr: T[]): T //리턴값에 제네릭 식별자 넣어주기
}

// 배열의 첫 번째 요소 반환하는 함수
const superPrint: SuperPrint = (arr) => arr[0];


const a = superPrint([1,2,3]);
const b = superPrint(["a","b","c"]);
const c = superPrint([true, false]);
const d = superPrint([1,"a", true]);

console.log(a);  //1
console.log(b);  //"a"
console.log(c);  //true
console.log(d);  //1
  • 인자에 string 타입의 배열을 보내면, 리턴 값도 string 타입이 나온다.

  • <T>(arr: T[]): T
    이렇게 타입스크립트에게 타입을 유추하도록 하면 된다.
    그러면 타입스크립트는 그 타입의 배열이 될 것이라는 것을 인지하고 그 타입 중 하나를 리턴한다.

  • 이렇게 superPrint()라는 같은 함수를 사용하지만, 콜 시그니처는 타입별로 다 다르게 사용할 수 있다.

any를 넣는게 낫지 않을까 생각 할수도 있지만 그건 아니다 ^^~

제네릭은 내가 요구한 대로 시그니처를 생성해 주는 도구라고 생각하면 된다.


Generic 타입 추가하기


제네릭을 하나 더 추가하고 싶다면 이렇게 하면 된다.

//꺽쇠 안에 제네릭 추가 작성
//사용할 곳에 제네릭 넣기: 두번째 인자
type SuperPrint = {
    <T, V>(a: T[], b: V): T
}
//그러면 타입스크립트는 제네릭을 처음 인식 했을 때와 제네릭의 순서를 기반으로 제네릭의 타입을 알게 된다.


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


//여기에서 요구에 따라 콜 시그니처가 생성된다.
const a = superPrint([1,2,3], "");
const b = superPrint(["a","b","c"], 1);
const c = superPrint([true, false], "");
const d = superPrint([1,"a", true], false);

console.log(a);
console.log(b);
console.log(c);
console.log(d);
  • 이런식으로 각각의 콜 시그니처가 생성된다.


//SuperPrint 타입 만들고, 콜 시그니처 작성
type SuperPrint = {
    <T, V>(a: T[], b: V): T
}
    
//함수에 SuperPrint 타입 적용
const superPrint: SuperPrint = (a) => a[0]

실제로는 내가 제네릭을 사용해서 직접 콜 시그니처를 만드는 일은 거의 없을거라고 한다.

  • 주로 제네릭을 통해 생성된 라이브러리나 패키지를 사용하게 되기 때문이다.
    즉, 다른 개발자가 사용할 기능을 개발하기 위해 라이브러리를 만들 경우에는 제네릭이 유용하다.

  • nextJS, nestJS, reactJS를 사용한다면, 대부분 제네릭을 사용만 하고 만들지는 않을 거라고 한다.

함수의 콜 시그니처 외에 다른 곳에서도 사용되는 제네릭


//SuperPrint 타입 만들고, 콜 시그니처 작성
type SuperPrint = {
    <T, V>(a: T[], b: V): T
}
    
//함수에 SuperPrint 타입 적용
const superPrint: SuperPrint = (a) => a[0]

이 부분을 일반 함수로 바꿀 수 있다.

//일반 함수
function superPrint<T>(a: T[]){
  return a[0];
}

//화살표 함수
const superPrint: <T> = (a:T[]): T => {
  return a[0]
}

제네릭을 사용하여 타입을 생성할수도 있고 타입을 확장할 수도 있다.

type BTS<T> = {
    name: string,
    age: number,
    extraInfo: T, //extraInfo 가 어떤 타입이든 될 수 있다면 제네릭을 만들고, 지정해주면 된다.
}

//BTS 타입을 가지는 rm 객체 생성
//extraInfo로 짜장면을 좋아한다고 알려줘 보자.
//제네릭에 {favoriteFood: string} 전달
const rm: BTS<{favoriteFood: string}> = {
    name: "RM",
    age: 28,
    extraInfo: {
        favoriteFood: "짜장면"
    },
}

이렇게 확장해서 사용할 수 있다.
아래 처럼 쪼개도 된다.

type BTS<T> = {
    name: string,
    age: number,
    extraInfo: T, //extraInfo 가 어떤 타입이든 될 수 있다면 제네릭을 만들고, 지정해주면 된다.
}

type BtsFavFood = BTS<{favoriteFood: string}>

const rm: BtsFavFood = {
    name: "RM",
    age: 28,
    extraInfo: {
        favoriteFood: "짜장면"
    },
}

더 쪼갤 수도 있다..^^

type BTS<T> = {
    name: string,
    age: number,
    extraInfo: T, //extraInfo 가 어떤 타입이든 될 수 있다면 제네릭을 만들고, 지정해주면 된다.
}

type RmExtra = { favoriteFood: string }

type BtsFavFood = BTS<RmExtra>

const rm: BtsFavFood = {
    name: "RM",
    age: 28,
    extraInfo: {
        favoriteFood: "짜장면"
    },
}

이런식으로 원하는대로 코드를 확장하는 것이 가능하다.

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

다른 객체를 하나 더 만들어보자.

진은 extrainfo가 없다고 해보자.
그러면 제네릭에 null을 전달하면 된다.

const jin: BTS<null> = {
    name: "JIN",
    age: 30,
    extraInfo: null,
}

타입을 이렇게 재사용할 수 있다.

  • 만약 많은 것을 가진 커다란 타입을 하나 가지고 있는데, 그 중 하나가 달라질 수 있는 타입이라면, 거기에 제네릭을 넣으면 된다.
  • 그러면 그 타입을 재사용할 수 있게 된다.

대부분의 기본적인 타입스크립트의 타입은 제네릭이다.


보통 대부분의 기본적인 타입스크립트의 타입은 제네릭으로 만들어져 있다.

  • 자바스크립트의 표준라이브러리를 보면 어레이를 만드는데 제네릭을 사용하고 있는 것을 볼 수 있다.
type A = Array<number> //제네릭에 number를 넣어 number로 된 Array라고 하자.

let a: A = [1, 2, 3, 4] //number로 된 어레이를 넣어보면 잘 작동한다.

숫자로 된 배열을 인자로 받는 함수가 있다고 해보자.

  • 그러면 인자의 타입을 아래 처럼 설정할 수 있다.
    둘 다 똑같다.
//인자로 들어오는 타입: number[]
function printAllNumbers(arr: number[]){
  arr.forEach( i => console.log(i));
};

//🔥 제네릭을 사용하여 인자로 들어오는 타입 설정: Array<number>
function printAllNumbers2(arr: Array<number>{
  arr.forEach( i => console.log(i));
}

리액트에서 제네릭을 사용하는 예시

//useState는 제네릭을 받는다.
//타입스크립트가 useState의 타입을 알 수 있도록 이런 식으로 작성해야 한다.
const [state, setState] = useState<number>();

//이렇게 제네릭을 보내면, useState의 콜 시그니처는 number타입의 useState가 된다.
profile
Always have hope🍀 & constant passion🔥

0개의 댓글