#1 Call Signatures
call signatures란 함수의 이름 위에 마우스를 올렸을 때 보여주는 것을 말한다. 즉, 함수를 어떻게 호출해야 하고(함수 인자의 타입), 어떤 것을 반환하는 지(함수 반환 값의 타입)를 보여주는 것을 call signatures라고 한다.
function add(a:number, b:number){
return a+b
}

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

그런데 이렇게 함수를 선언할 때마다 인자와 반환값의 데이터 타입을 선언해주는 것은 귀찮을 수 있다.
따라서 데이터 타입을 미리 선언해두고 이를 사용할 수 있다.
type Add = (a:number, b:number) => number;
const add:Add = (a, b) => a + b
이를 통해 프로그램을 디자인하면서 타입을 먼저 생각하고, 그 후에 코드로 구현할 수 있게 된다.
#2 Overloading
오버로딩은 함수가 서로 다른 여러개의 call signatures를 가지고 있을 때 발생시킨다.
type Add = {
(a:number, b:number) : number
(a:number, b:string) : number
}
const adddd: Add= (a,b) => {
if(typeof b === 'string') return a
return a+b
}
위와 같이 Add에 call signatures가 여러개 있을 때에는 함수에 명시를 해주는 부분이 필요하다.
그런데 이와 같은 경우는 좋지 못한 경우다. 단지 함수의 overloading이 무엇인지에 대해 알기 위해 보여준 것이고, 저렇게 함수를 쓰는 경우는 흔치 않다.
우리가 실제 개발을 하면서 만날 수 있는 오버로딩에 대해 알아보자.
type Config = {
path: string,
state: object
}
type Push = {
(path: string): void
(config: Config): void
}
const push: Push = (config) => {
if(typeof config === "string") {console.log(config)}
else{
console.log(config.path, config.state)
}
}
Nextjs(페이지를 넘기는 프레임워크(?))의 일부 기능을 간단하게 구현한 것이다.
push 함수 내부에서 config의 type에 맞게 if나 else문을 실행하게 된다. 즉, string을 받거나 config 객체를 받을 수 있을 때 경우에 따라 다른 코드를 실행시키는 것이다.
type Add = {
(a:number, b:number) :number
(a:number, b:number, c:number) :number
}
const add:Add = (a, b, c?:number) =>{
return a + b
}
위와 같이 call signatures에서 인자의 개수가 다른 경우도 구현할 수 있다. add 함수에서 c는 optional하기에 ?를 붙여주고 인자 type을 number로 표시해주었다.
#3 Polymorphism
Poly – many, several, much, multi
morphos – form, structure
⇒ polymorphos → 여러 다른 구조, 다형성
어떠한 배열을 받아서 배열의 요소들을 하나씩 출력하는 함수를 만들어보자.
type SuperPrint = {
(arr: number[]):void
(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"]) //error
위와 같은 함수를 통해서 숫자와 boolean으로 이루어진 배열을 하나씩 출력할 수 있다.
하지만 string은 call signatures에 정의되지 않았기에 error를 발생시킨다.
concrete type → number, string, boolean과 같이 기본적으로 사용해오던 type들
generic type → placeholder of type
generic type은 call signautre를 작성할 때, 어떤 타입이 들어오게 될지 모르는 경우에 사용한다.
type SuperPrint = {
<typeHolder>(arr: typeHolder[]): typeHolder
}
const superPrint: SuperPrint = (arr) => arr[0]
const a = superPrint([1,2,3,4])
const b = superPrint([true,false,true])
const c = superPrint(["a","b","c"])
const d = superPrint([1,true])
const e = superPrint([1,"a", false])
위와 같이 선언하면, 우리는 타입스크립트에게 타입을 유추하도록 알려준 것이다.
실제로 커서를 올려봤을 때, 타입스크립트가 타입을 유추해서 우리에게 signature를 보여주는 것을 확인할 수 있다.



위의 코드는 아래와 같이 작성해도 똑같이 작동한다.
type SuperPrint = {
<T>(arr: T[]): T
}
const superPrint: SuperPrint = (arr) => arr[0]
const a = superPrint([1,2,3,4])
const b = superPrint([true,false,true])
const c = superPrint(["a","b","c"])
const d = superPrint([1,true])
const e = superPrint([1,"a", false])
즉, generic의 이름은 내가 원하는 대로 지정하면 된다.
#4 Generics Recap
generic을 사용하는 것과 any로 선언하는 것의 차이는 무엇일까?
generic은 우리가 선언한 대로 typescript가 추론하여 이를 설정하는 것이지만, any의 경우는 정말 말그대로 아무거나 와도 된다고 받아들이는 것이다.
따라서 any를 사용하는 경우에는 다음과 같은 문제가 발생할 수 있다.
type SuperPrint = {
(a: any[]) :any
}
const superPrint: SuperPrint = (arr) => arr[0]
const a = superPrint([1,2,3,4])
const b = superPrint([true,false,true])
const c = superPrint(["a","b","c"])
const d = superPrint([1,true])
const e = superPrint([1,"a", false])
e.toUpperCase() //에러를 발생시키지 않는다!
SuperPrint에서 any 배열을 받아서 any를 반환하고 있기에, a, b, c, d, e는 모두 any가 되고, 결국 toUpperCase() 함수는 어떤 것(number? string? boolean?)에 대해 실행하는 지도 모르면서 에러를 발생시키지 않게 된다.
하지만 generic을 사용하면 이러한 오류를 막을 수 있다.
type SuperPrint = {
<T>(arr: T[]): T
}
const superPrint: SuperPrint = (arr) => arr[0]
const a = superPrint([1,2,3,4])
const b = superPrint([true,false,true])
const c = superPrint(["a","b","c"])
const d = superPrint([1,true])
const e = superPrint([1,"a", false])
e.toUpperCase() //error!
위의 경우에 e는 우리가 작성해준 대로 typescript가 추론하여 number | string | boolean 형식이 되었는데, number에 대해서는 toUpperCase() 속성이 없으므로 오류를 출력해준다.
generic은 여러개를 사용할 수 있다.
type SuperPrint = {
<T, M>(arr: T[], b: M): T
}
const superPrint: SuperPrint = (arr) => arr[0]
const a = superPrint([1,2,3,4], "x")
타입스크립트는 generic을 처음 인식했을 때와 generic의 순서를 기반으로 generic의 타입을 알게 된다. 즉, 내가 직접 작성한 코드를 기반으로 Typescript가 추론하여 알아내는 것이다.
위와 같이 generic을 두개 선언해서 b까지 추가로 선언한다면, superPrint 함수를 호출할 때 반드시 인자를 두개 전달해야 오류를 피할 수 있다.
generic을 다른 곳에서도 사용하는 예시에 대해 살펴보자.
type Player<E> = {
name: string
extraInfo: E
}
type JinExtra = {
favfood:string
}
type JinPlayer = Player<JinExtra>
const Jin: JinPlayer ={
name: 'Jin',
extraInfo: {
favfood: 'chicken'
}
}
<E>라는 generic을 활용해서 우리가 입력한 코드에 따라 typescript가 추론하도록 만들었다.
위의 JinPlayer 부분에서 Player<JinExtra>로 <E> 부분에 JinExtra가 들어가는 것을 명시해주었고, 따라서 typescript는 <E>가 JinExtra 객체를 받는다는 것을 추론할 수 있게 된다.
const Heo: Player<null> = {
name: 'Heo',
extraInfo: null
}
위와 같은 코드도 추가할 수 있게 된다! 이렇게 Player 타입을 다양하게 재사용할 수 있다.
** 본 글은 노마드코더의 ‘타입스크립트로 블록체인 만들기’ 강의를 바탕으로 작성했습니다. **