📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[TypeScript] 타입스크립트 함수 타입 정의 방법

함수 타입 정의 방법

타입스크립트는 일반적인 변수에서뿐만 아니라 함수에도 타입을 정해줘야 한다.

일단 생각해 보자. 함수에 타입을 줄만한 값이 뭐가 있을까? 바로 매개변수리턴값이다.

함수에서는 매개변수와 리턴값에 각각 타입을 부여해 줄 수 있으며, 기본 타입과 똑같이 다른 함수 타입과 호환되기도 한다.

 

일반적인 방법

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

// 결과값에는 타입을 따로 적지 않아도 자동으로 리턴값의 타입을 추론해준다.
function func(a: number, b: number) {
    return a + b;
}

// 화살표 함수도 똑같다.
const add = (a: number, b: number) => a + b;

위 코드는 함수에 타입을 부여하는 가장 일반적인 방법이다. 각각의 매개변수에 모두 타입을 부여해줘야 하고, 리턴값에도 따로 타입을 부여해줘야 한다. 하지만, 리턴값은 자동으로 타입이 추론되기 때문에 따로 적지 않아도 무관하다.

 

선택적 매개변수

// 매개변수 = '값'을 하면 따로 인수에 값을 넣지 않아도 자동으로 디폴트 값으로 나옴
// 옵셔널 체이닝을 이용하면 선택적 매개변수를 만들 수 있음
function getMyItem(item = 'bag', color?: string) {
    console.log(`Item is ${item}`);
}
getMyItem(); // Item is bag

// 선택적 매개변수를 사용하려면 타입 좁히기가 필수
function getMyItem(item = 'bag', color?: string) {
    if (typeof color === 'string') {
        console.log(color.toUpperCase());
    }
}

매개변수 = '값'은 함수 매개변수의 기본값을 설정해 주는 것으로 따로 인수에 값을 넣지 않아도 자동으로 리턴되는 값이다.

함수의 매개변수를 선언할 때, 선언된 모든 매개변수를 사용하지 않는 경우도 많다. 이럴 때는 옵셔널 체이닝을 이용해서 선택적 매개변수를 만들어주면 된다. 선택적 매개변수를 만들 때는 반드시 필수 매개변수 뒤에 위치해야 한다.

 

rest 파라미터의 타입은 배열로 선언해 주기

// ...rest의 경우 배열로 타입을 선언
function (...rest: number[]) {
    let sum = 0;
    rest.forEach(it => sum += it);
    return sum;
}

// ...rest 배열의 길이까지 고정하고 싶다면 튜플로 선언
function (...rest: [number, number, number]) {
    let sum = 0;
    rest.forEach(it => sum += it);
    return sum;
}

rest 파라미터는 함수 파라미터의 나머지 요소들을 나타내는 파라미터로 배열로 받아온다는 특징이 있다. 따라서, 타입을 선언해 줄 때도 rest 파라미터가 배열이기 때문에 배열 혹은 튜플로 선언해 준다.

 

타입 별칭으로 함수 타입 정의하기

// 방식 ① : 함수 타입 표현식
type Operation = (a: number, b: number) => number;

// 방식 ② : 호출 시그니쳐 (콜 시그니쳐)
type Operation = {
    (a: number, b: number): number
}

// 함수 형태가 똑같을 경우 굳이 하나씩 타입 정의할 필요X
const add: Operation = (a, b) => a + b;
const sub: Operation = (a, b) => a - b;
const multiply: Operation = (a, b) => a * b;
const divide: Operation = (a, b) => a / b;

객체의 타입 부여 방식을 배울 때, 타입 별칭을 이용한 적이 있었는데 함수 또한 타입 별칭을 이용해서 똑같이 생긴 형태의 함수끼리는 타입을 공유할 수 있도록 하는 방법이 있다. 위 코드를 보면 4개의 사칙연산 함수는 모두 number 타입의 매개변수가 2개이고, 리턴값도 number 타입의 똑같은 형태를 취하고 있다. 이 함수들은 모두 똑같은 형태이므로 굳이 모든 함수에 타입을 부여할 필요가 없이 하나의 타입 별칭을 만들어서 사용해 준다.

 

타입 별칭을 사용하는 방식에는 2가지가 있다. 하나는 함수 타입 표현식이고 나머지 하나는 호출 시그니쳐이다.

함수 타입 표현식은 말 그대로 함수 표현식처럼 타입을 정의한다. 값 자리에 타입이 들어간다는 것 빼고는 화살표 함수와 똑같이 생겼다.

 

호출 시그니쳐 혹은 콜 시그니쳐는 객체에 함수 타입을 담는 형식이다. 함수도 객체이기 때문에 객체처럼 함수 타입 정의가 가능하다.

 

함수 타입의 호환성

이전에 타입의 호환성을 배우면서 슈퍼 타입과 서브 타입, 업캐스팅과 다운캐스팅을 본 적이 있다.

슈퍼 타입이 서브 타입에 들어가는 것을 다운캐스팅, 서브 타입이 슈퍼 타입에 들어가는 것을 업캐스팅이라고 했었다.

일반적으로 서브 타입에 슈퍼 타입이 들어가는 다운캐스팅의 경우 호환의 문제로 오류가 발생한다.

type A = () => number;
type B = () => 10;

let a: A = () => 10;
let b: B = () => 10;

a = b;
b = a; // Error! b는 리터럴 타입이고 a는 숫자 타입이므로 다운캐스팅

A는 숫자 타입, B는 숫자 리터럴 타입이다. 타입 관계를 봤을 때는 A가 슈퍼 타입, B가 서브 타입에 속한다.

따라서, a에 b가 들어갈 수는 있지만 b에 a가 들어갈 수는 없다. 

단, 이것은 매개변수를 모두 무시한 채 리턴값의 타입만 비교했을 때의 경우라는 점을 알아두자.

type A = (value: number) => void;
type B = (value: 10) => void;

let a: A = (value) => {};
let b: B = (value) => {};

a = b; // Error! 업캐스팅인데 오류?
b = a;

이번엔 매개변수의 타입을 비교해 보자. A의 매개변수는 숫자 타입, B의 매개변수는 숫자 리터럴 타입이다.

그럼 A가 슈퍼 타입이 되고 B가 서브 타입이 되기 때문에 b의 자리에 a가 들어오면 다운캐스팅으로 인해 오류가 난다고 생각할 것이다. 하지만, 결과는 업캐스팅일 때 오류가 난다.

 

매개변수의 호환성을 바라볼 때는 조금 다르게 접근해야 한다. 예를 들어, number 타입인 A타입에는 1, 2, 3, 4, 5... 등의 여러 숫자가 들어올 수 있고 리터럴 타입인 B에는 숫자 10만 들어올 수 있다. a에는 10도 있고 b에는 10만 있다.

a 자리에 b가 들어올 경우 a 자리에는 10을 포함한 여러 숫자가 들어있기 때문에 b의 10을 대체할 수가 있다.

하지만, b 자리에 a가 들어갈 경우 b 자리에는 고작 10만 있기 때문에 a가 가지고 있는 1, 2, 3.. 은 들어갈 수가 없다.

따라서, b에 a는 들어올 수 있지만 a에는 b가 들어올 수 없는 것이다.

 

매개변수의 타입을 비교할 때는 기존의 방식대로 보지 말고 누가 더 적은 값을 가지고 있냐에 초점을 맞추면 된다.

더 적은 값을 갖고 있는 타입이 슈퍼 타입이 되고 많은 타입이 서브 타입이 된다. (객체 타입 호환과 똑같다.)

이러한 원리는 모두 함수가 객체이기 때문에 나타나는 현상이며, 쉽게 기억하려면 그냥 슈퍼 타입과 서브 타입이 기존과 반대라고 생각하자. (위 설명들은 함수 타입의 호환에 대한 정석적인 설명은 아니며, 오로지 이해하기 위한 비유이다.)

type Func1 = (a: number, b: number) => void;
type Func2 = (a: number) => void;

let func1: Func1 = (a, b) => {};
let func2: Func2 = (a) => {};

func1 = func2;
// func2 = func1; Error! 매개변수 갯수가 더 많은 게 슈퍼 타입

매개변수의 개수만 놓고 비교했을 때는 매개변수의 개수가 많은 게 슈퍼 타입이고 더 적은 게 서브 타입이다.

 

함수 오버로드

// 버전 만들기 (오버로드 시그니쳐)
function func(a: number): void;
function func(a: number, b: number, c: number): void;

// 실제 구현부 (구현 시그니쳐)
function func(a: number, b?: number, c?: number) {
    if (typeof b === 'number' && typeof c === 'number') {
        console.log(a + b + c);
    } else {
        console.log(a);
    }
}

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

함수 오버로드하나의 함수를 매개변수의 개수나 타입에 따라 여러 가지 버전으로 만드는 문법을 말한다.

같은 이름의 함수를 매개변수만 다른 채로 선언해 주면 선언된 매개변수의 타입과 개수에 맞는 함수만 실행이 된다.

같은 함수인데 호출되는 매개변수만 다를 경우 사용하면 유용하다.

 

1. function func(a: number): number => {...} // 리턴값 타입은 생략 가능
2. const func = (a: number): number => {...} // 화살표 함수도 똑같음
3. type Func = (a: number, b: number) => number; // 함수의 타입 별칭
4. rest 파라미터는 배열로 취급하기 때문에 타입도 배열로 선언
5-1. 리턴값을 기준으로 호환성을 따졌을 때는 기존의 방식대로.
5-2. 매개변수를 기준으로 호환성을 따졌을 때는 기존의 방식과 반대로. (객체의 호환 원리와 같음)
5-3. 매개변수의 개수를 기준으로 할 때는 매개변수가 많은 게 슈퍼 타입.
6. 함수 오버로드란 같은 함수인데 매개변수만 다를 경우를 대비하여 버전을 만들어 놓는 것