Front-End/TypeScript

타입스크립트[TypeScript] | 함수 Function 타입 정의하기

jaeyeong 2023. 4. 7. 15:11

 

해당 글은 타입스크립트 프로그래밍 책과 유데미 타입스크립트 강의를 공부하여 정리한 글입니다.

수정해야 할 부분이 있다면 언제든지 알려주세요!

 

함수의 타입 정의하기

함수의 타입을 정의할 때 Function을 사용하지 않는다.
왜냐하면 Object에 모든 객체가 담기는 것 처럼 모든 함수의 타입을 뜻하며 특정 함수와 타입에 대해서는 알려주지 않기 때문이다.

만약 다음과 같은 함수를 정의하기 위해선 어떻게 해야할까?

function sum (a: number, b: number): number {
    return a+b;
}


매개변수의 타입과 반환 타입을 사용하여 표현할 수 있다

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


책에서는 이를 (단축형) 호출 시그니처 또는 타입 시그니처라고 부른다고 한다.
호출 시그니처를 사용하여 함수 표현식을 더욱 간단하게 정의할 수 있다.

type Log = (message: string, userId?: string) => void;

let log: Log = (message, userId = 'Not signed in') => {
    let time = new Date().toISOString();
    console.log(time, message, userId);
}

log('Jae', 'yeong'); // "2023-03-28T06:22:44.852Z", "Jae", "yeong"


log 함수를 정의할 때 Log타입을 전달했으므로 매개변수에 타입을 다시 지정할 필요가 없다.
만약 호출 시그니처가 여러개인 함수를 정의해야한다면 전체 호출 시그니처를 사용하는 것이 좋다.

 

전체 호출 시그니처

호출 시그니처는 type Log = (message: string, userId?: string) => void 형식으로 작성한다면
전체 시그니처는 호출 시그니처가 중괄호로 한번 더 감싸진 형식으로 작성한다.

type Log = {
	(message: string, userId?: string) => void
}


책에서는 간단한 함수에는 호출 시그니처를 사용하되 복잡한 함수라면 전체 시그니처를 사용할 것을 권장하고 있다.
(곧 이 얘기를 이해하게 될 것이다!)

 

오버로드 된 함수 타입

나는 오버로드를 기존의 것을 덮어씌운다는 개념으로 알고있었기 때문에

오버로드 된 함수 타입을 딱 봤을 때 기존의 타입을 덮어씌워서 다른 타입으로 수정하는 것이라고 생각했다.


하지만 책에서는 호출 시그니처가 여러 개인 함수라고 설명하고 있었다.

'호출 시그니처가 여러 개인 함수' 라는 말의 의미를 생각해봤을 때,

호출 시그니처를 작성하기 위해 필요한 것은 파라미터의 타입반환 타입인데, 이 요소들이 여러 개인 함수를 뜻하는 것이라고 생각되었다.

 

나는 책을 읽으면서 대부분 바로 이해하면서 읽었었는데, 이 주제는 여러번 다시 읽을 정도로 이해가 잘 안됐었다.

1. 예제에서 타입을 정의하는데, 반환값의 타입이 어디서 온건지 헷갈렸다.
(다시 생각해보면 예시일 뿐이어서 크게 생각하지 않아도 될 부분이었다)

2. 조건문을 나누어 상황에 따른 로직을 분리하였는데 반대로 되어있는 것 같아서 헷갈렸다.
(이 부분 또한 예시로 로직을 작성한 것이기 때문에 크게 생각하지 않아도 될 부분이었다)

 

뭔가 찝찝하게 헷갈리는 부분들을 정리해보고 제외하니, 크게 어려운 부분은 아니였다.

책에 있는 예제를 [TS playground]에서 따라 작성해보면서 이해하려고 했고, 내가 다시 작성해보면서 이해한 부분을 공유하려고 한다.

 

[이 글]을 읽고 이해하는데 도움이 되었다.

 

오버로드 된 함수 구현하기

옷의 정보를 입력받는 함수를 구현한다고 해보자.

나는 이렇게 함수 정의를 할 것 같다.

const getClothesInfo = (name: string, price: number, gender: string) => {
    return `이 옷은 ${gender}복이며 이름은 ${name}이고 가격은 ${price}원 입니다.`;
}


그리고 이 함수를 단축 호출 시그니처로 타입 정의 를 한다면 다음과 같다.

type Clothes = (name: string, price: number, gender: string) => string

const getClothesInfo:Clothes = (name, price, gender) => {
    return `이 옷은 ${gender}복이며 이름은 ${name}이고 가격은 ${price}원 입니다.`;
}

 

만약 가격의 정보를 숫자 값으로 받고 있지만 문의가 필요한 상품일 경우엔 값을 안내하지 않는다고 생각해보자.

(가격 인자를 받지 않는 다는 말)

이 때는 단축 호출 시그니처로 표현할 수 없기 때문에 전체 시그니처로 표현해야 한다.

type Clothes = {
    (name: string, price: number, gender: string): string,
    (name: string, gender: string): string
}

const getClothesInfo:Clothes = (name, price, gender) => {
    return `이 옷은 ${gender}복이며 이름은 ${name}이고 가격은 ${price}원 입니다.`;
}

 

이 때 TS는 오류를 발생시킨다.
Type '(name: string, price: number, gender: string) => string' is not assignable to type 'Clothes'.

TS는 함수에 여러개의 오버로드 시그니처를 선언하면, 함수의 타입은 오버로드 시그니처들의 유니온이 되는데,
이 조합된 시그니처는 자동으로 추론되지 않기 때문에 직접 선언해주어야 한다.

그래서 함수 선언부에 다음과 같이 타입을 수동으로 결합해주어야 한다.

const getClothesInfo:Clothes = (
    name: string,
    price: number | string,
    gender?: string
) => {
    ...
}


내가 헷갈렸던 부분은 바로 여기다.
(책에 있는 예시는 내가 작성한 함수와 다르지만)

"price 값을 받는다면 number타입을 받고, price값을 받지 않을 땐 null이나 undefined를 받아야하는게 아닌가? 왜 string이 오지?" 라는 의문이 있었다.

책을 여러번 읽고 다른 글을 참고하니 이해가 되었는데,

함수 호출부에서 모든 인자를 받는다면 두번째 값은 price 값을 받게 된다.

만약 price의 값을 받지 않는다면 name, gender값만 받게 되고, 두번째 값은 gender 값으로 받게 된다.

 

price 대신 gender가 받아지기 때문에 price 타입은 number 또는 string이 정의되는 것이다.
또한 이럴 경우에 세번째 값인 gender자리는 비어있게 되기 때문에 ?를 붙여 값이 전달 될 수도 있고, 안 될 수도 있다는 것을

타입스크립트에게 알려주고 있다.

그리고 함수 구현부에는 타입에 따라 어떻게 반환되어야 할지 적어주어야 한다.
모든 것들을 반영한 코드는 다음과 같다.

type Clothes = {
    (name: string, price: number, gender: string): string,
    (name: string, gender: string): string
}

const getClothesInfo:Clothes = (name: string, price: number | string, gender?: string) => {
    if (typeof price === 'number') {
        return `이 옷은 ${gender}복이며 이름은 ${name}이고 가격은 ${price}원 입니다.`;
    }
    return `이 옷은 ${gender}복이며 이름은 ${name}이고 가격은 문의해주세요. 🙏🏻`;
}

 

오버로드 된 함수로 함수의 파라미터 만들기

전체 타입 시그니처는 오버로딩 된 함수를 구현할 때도 쓰이지만, 함수의 파라미터를 만들 때도 사용할 수 있다.
아래 예시는 책에 나와있는 예시이다.

type WarnUser = {
    (warning: string): void
    wasCalled: boolean
}

const warnUser: WarnUser = (warning) => {
    if(warnUser.wasCalled){
        return;
    }
    warnUser.wasCalled = true;
    alert(warning);
}

warnUser.wasCalled = false;

 

이렇게 작성할 경우 warnUser는 호출할 수 있는 함수이면서 동시에 boolean 속성인 wasCalled를 가지기도 한다.