[Typescript] Call signature과 Generic 타입

Jennifer Heejin Kang·2022년 10월 20일
1

Typescript

목록 보기
3/4

Call Signature

타입스크립트의 중요 개념 중 하나인 call signature에 대해서 알아보자. 콜 시그니처란 함수 위에 커서를 올리면 나오는 파라미터와 리턴 타입 정보를 말한다. 콜 시그니처는 함수를 만들기 전에 먼저 다음과 같이 타입을 정할 수 있다.

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

여기서 Add 타입는 a 숫자값과 b 숫자값을 더한 값을 리턴하기 위한 함수의 타입이다. 이렇게 먼저 타입을 정하면 다음과 같이 함수를 구현할 수 있다.

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

이렇기 때문에 함수를 구현할 때 파라미터나 리턴값의 타입을 따로 고려하면서 작성할 필요가 없다. 즉, 장점이라 하면 디자인 시에 타입을 결정해야하기 때문에 개발자가 타입을 생각하고 작성할 수 있게 한다.
타입스크립트의 특징은 바로 자바스크립트 형태로 컴파일된다는 것인데 얘는 자바스크립트로 컴파일되지 않는다.

여러 call signature에 의한 overloading

하나의 함수는 여러가지 call signature를 가질 수 있다. 함수명은 갖지만 변수 타입이 다른 경우를 위해서다.

type Add = {
    (a: number, b:number): number
    (a: string, b:number): number
}
const add:Add = (a,b) => a+b // ERROR!

이렇게 여러개의 call signature을 가질 수 있는데 마지막 줄은 에러가 발생한다. 왜? a의 타입은 (number | string)인데 string과 number는 더할 수가 없어서 타입스크립트가 "너 뭐하니?"하면서 에러를 보낸다.

아니 여러 call signature를 한 함수에 넣을 수 있다고 했으면서 뭐 어떻게 하라는건가 싶을 수 있다. 이럴 땐 간단히 변수의 타입별로 각각 처리를 해주면 된다. 이어서 보자.

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

const add:Add = (a,b) => {
    if(typeof a === 'number'){
        return a+b
    }
    else return b
}

이렇게 a 타입이 number일 때는 둘을 더하고 string일 때에는 b를 리턴하는 것으로 구현했다. 그럼 타입스트립트가 "오호 타입을 제대로 이해하고 썼구나?"하면서 넘어간다.
근데 두개의 숫자만 더하는게 아니라 세 숫자를 더하고 싶을 때도 있지 않겠는가? 그러면 optional 타입을 이용해서 함수를 만들어주면 된다.

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

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

여기서 세번째 변수가 있을수도, 없을수도 있기 때문에 세번째 변수는 optional 변수로 설정하고 꼭 타입을 적어줘야한다.

Polymorphism (다형성)

타입스크립트는 여러 형태의 구조/함수를 가질 수 있다는 특징이 있다. 다음 예시를 보자.

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

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

const num = superPrint([1,2,3,4]) // OK
const string = superPrint(["a","b","c"]) // OK
const boolean = superPrint([true, false]) // ERROR!!

call signature을 이용해서 함수의 타입을 먼저 결정할 수 있는데, 동일한 이름을 가진 변수들에 다른 타입을 부여하면 타입스크립트가 부여한 타입을 인식하고 맞게 실행한다.
여기서 마지막 줄을 살펴보자. call signature에 boolean 타입을 추가하지 않았기 때문에 당연히 에러가 발생한다. 근데 여기서, 과연 call signature에 boolean을 추가하는 것이 답일까?
물론 그렇게 해도 동작은 할 것이지만, 프로그래밍 하면서 모든 타입을 작성하기라는건 너무 비효율적이고 놓치는 부분이 생길 수 있다. 그 경우를 대비해서, Generic(제네릭) 타입을 사용하면 훨씬 편리하게 타입 걱정 없이 사용할 수 있다.

Generic Type

타입에는 크게 두 종류가 있다. concrete type과 generic type.
Concrete 타입은 우리가 일반적으로 아는 number, string, void 등 정해져있는 타입을 말한다. 한번 설정하면 해당 타입은 변경할 수 없다.
이와는 다르게 Generic타입은 개발자가 타입을 직접 알려주지 않아도 타입스크립트가 알아서 추론해주는 타입이다. placeholder 역할을 하고 주로 call signature에 사용된다.
위에서 보여준 예시는 작동은 예상대로 동작하나 개발자 입장에서 비효율적이고 지저분한 코드를 작성하는 셈이다. 그래서, 제네릭 타입을 이용해서 다음과 같이 간단하게 표현할 수 있다.

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

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

const num = superPrint([1,2,3,4]) // OK
const string = superPrint(["a","b","c"]) // OK
const boolean = superPrint([true, false]) // OK
const whatever = superPrint([1,2,true,"what"]) // 심지어 이것도 된다!

문법을 살펴보면 <>안에 제네릭 타입 명을 넣고 해당 타입을 사용할 변수명 앞에 나타낸다. 주로 , 등을 사용하는데 상관없다. 이렇게 하면 해당 변수는 어떤 타입의 변수든 상관없이 다 받아드릴 준비가 된 것이다. 쏘 쿨하다.

Any 타입 대신에 사용하는 이유?

어짜피 Any 타입도 타입이 정해지지 않은 것인데 굳이 Generic 타입을 사용해야하나? 왜 Any는 안되지?라는 생각을 가질 수 있다.
간단하다. Any 타입은 "보호받을 수 없는 타입"이다.

type SuperPrint = {
    (arr: any): any
}

SuperPrint call signature을 이렇게 작성하고 위에 코드를 돌려보면 문제없이 동작하기는 한다. 대신, 각 array들이 number[], boolean[] 타입으로 정해지는 것이 아니라 그냥 any 타입으로 남아있게 된다.
우리가 typescript를 쓰는 이유는? 바로 타입을 보호받기 위해서 사용하는 것인데, any를 씀으로 인해서 더이상 보호받지 못하고 javascript과 동일하게 동작해서 오류 발견이 어렵기 시작한다. 타입스크립트에서 "내가 알아서 다 해줄게 걱정마!"하고 제네릭 타입을 제공해줬는데 굳이 any를 써야할까?

제네릭타입은 많은 패키지들에서 사용되고 있다. 왜냐면 개발 프로젝트에 따라 변수가 다르게 필요할 수 있으니, 어떤 타입의 변수든 간에 처리를 해줘야 하는 상황이 생기기 때문이다! 그러니 제네릭 타입은 타입스크립트에서 굉장히 유용하고 똑똑한 친구이다. 잘 기억하고 필요 시에 제대로 써먹어야겠다 :>

profile
초짜에서 벗어나 개발 전문가가 되고 싶은 블로그 삐약이 🐥

0개의 댓글