본문 바로가기

함수 타입 정의

함수의 매개변수와 반환값에 타입을 붙이고, 선택적과 기본, 나머지 매개변수의 모양을 익힌 뒤, 함수 자체의 타입을 별칭으로 적어 문맥으로 매개변수의 타입을 정하는 방법과 타입이 지키지 못하는 값의 조건을 확인합니다.

함수는 입력을 받아 일을 하고 결과를 돌려주는 코드의 묶음입니다. 자바스크립트의 함수는 값처럼 변수에 담거나 다른 함수에 넘길 수 있는데, 이렇게 값으로 다룰 수 있는 것을 일급 객체(first-class object)라고 합니다. 타입스크립트는 이 유연함을 그대로 두면서, 함수가 무엇을 받고 무엇을 돌려주는지를 타입으로 적게 합니다.

용어 하나를 먼저 구분해 둡니다. 매개변수(parameter)는 함수를 선언할 때 적는 입력의 자리 이름이고, 인수(argument)는 함수를 호출할 때 그 자리에 실제로 넘기는 값입니다. 예제는 2장처럼 ts-first 프로젝트에 functions.ts를 만들고 include를 그 파일로 바꾸어 확인합니다.

add(5, 10)은 15를 반환합니다. optional은 undefined 가능성을 남기고 default는 undefined를 기본값으로 바꾸며 rest는 남은 인수를 배열로 받습니다.

TYPE / RUNTIME / CONTRACT

호출 타입을 검사하고 실제 인수로 실행합니다

add의 숫자 계약은 호출 전에 검사됩니다. 선택·기본·나머지 매개변수는 각자 다른 방식으로 본문에 값을 전달합니다.

정적 호출 계약과 실제 값 처리호출인수→정적함수계약→실제본문의3노드2간선개념연결이다. 타입검사와런타임값검증은다르며emit정책을자동강제하는실행도식이아니다.
호출 인수개수 · 정적 타입
연결 관계호출 인수 · 개수 · 정적 타입 → 함수 계약 · 매개변수 · 반환
함수 계약매개변수 · 반환
연결 관계함수 계약 · 매개변수 · 반환 → 실제 본문 · 값 조건 · 실제 결과
실제 본문값 조건 · 실제 결과
정적 호출 계약과 실제 값 처리 · 모바일호출인수→정적함수계약→실제본문의3노드2간선개념연결이다. 타입검사와런타임값검증은다르며emit정책을자동강제하는실행도식이아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.
호출 인수개수 · 정적 타입
연결 관계호출 인수 · 개수 · 정적 타입 → 함수 계약 · 매개변수 · 반환
함수 계약매개변수 · 반환
연결 관계함수 계약 · 매개변수 · 반환 → 실제 본문 · 값 조건 · 실제 결과
실제 본문값 조건 · 실제 결과

CALL

숫자 두 개와 숫자 결과

add(5, 10)은 15입니다. 숫자 자리에 문자열을 넣거나 숫자 반환 함수에서 문자열을 반환하면 타입 오류입니다.

OPTIONAL / DEFAULT

누락과 기본값을 구분

lastName?은 생략될 수 있습니다. buildName의 truthiness 검사는 빈 문자열도 제외합니다. greet의 기본값은 undefined일 때 적용됩니다.

REST

남은 인수를 배열로 수집

sumAll(...numbers: number[])은 1·2·3을 받아 6을 반환합니다. 이 예제의 나머지 인수는 모두 숫자여야 합니다.

FUNCTION TYPE

함수 값도 같은 계약으로 검사

AddFunction에 대입한 myAdd와 myMultiply는 숫자 매개변수 두 개와 숫자 반환을 지킵니다. 함수 타입의 화살표는 구현 문법과 별개입니다.

정적 검사 과정과 런타임 값 처리는 구분됩니다. contextual void에서 반환값을 무시하는 규칙은 숫자 반환 계약과 다른 규칙입니다.

위 그림처럼 함수를 호출하면, 컴파일러는 먼저 인수의 개수와 타입이 매개변수와 맞는지 검사하고, 실행할 때는 실제 인수의 값으로 본문이 돌아갑니다. 검사를 통과했다는 것과 본문이 받은 값이 업무의 조건에 맞는다는 것은 서로 다른 문제라는 점을 이 글의 끝에서 다시 봅니다. 그림의 선택적 매개변수 설명은 buildName이 값의 참 거짓을 보는 검사로 빈 문자열까지 거른다고 적었지만, 아래의 buildName은 lastName === undefined로 생략된 경우만 확인하므로 buildName("이", "")는 뒤에 공백이 붙은 "이 "를 돌려줍니다.

매개변수와 반환값의 타입

매개변수의 이름 뒤에 콜론과 타입을 쓰면 그 자리에 받을 값의 종류를, 괄호 뒤에 콜론과 타입을 쓰면 돌려줄 값의 종류를 정합니다. 함수 선언뿐 아니라 함수를 변수에 담는 함수 표현식과 화살표 함수에도 같은 방식으로 씁니다.

function add(x: number, y: number): number {
  return x + y;
}

function logMessage(message: string): void {
  console.log(message);
}

const subtract = function (a: number, b: number): number {
  return a - b;
};

const multiply = (a: number, b: number): number => a * b;

console.log(add(5, 10));
logMessage("TypeScript 함수 타입 학습");
console.log(subtract(20, 7), multiply(3, 4));
15
TypeScript 함수 타입 학습
13 12

값을 돌려주지 않는 logMessage는 반환 타입을 void로 적었습니다. 반환 타입은 생략해도 컴파일러가 return 문에서 추론하지만, 적어 두면 본문에서 실수로 다른 값을 돌려줄 때 함수가 정의된 자리에서 바로 진단을 받습니다.

컴파일러는 호출할 때 인수의 타입과 개수를 모두 검사합니다. 다음 세 줄은 각각 인수의 타입이 틀린 경우와 인수가 모자란 경우, 반환값의 타입이 틀린 경우입니다.

add("hello", 5);
add(1);
function invalidReturn(): number {
  return "이것은 숫자가 아닙니다.";
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2554: Expected 2 arguments, but got 1.
error TS2322: Type 'string' is not assignable to type 'number'.

자바스크립트에서는 인수를 덜 넘기면 남은 매개변수가 undefined가 되고, 더 넘기면 남는 인수가 조용히 버려집니다. 타입스크립트는 매개변수에 적은 개수와 다르게 부르는 호출을 TS2554로 알려 주므로, 인수를 빠뜨리거나 하나 더 넘기는 실수를 실행 전에 잡을 수 있습니다.

선택적 매개변수와 기본 매개변수

어떤 인수는 넘기지 않아도 되도록 만들고 싶을 때가 있습니다. 매개변수 이름 뒤에 물음표를 붙이면 선택적 매개변수(optional parameter)가 되고, 등호로 기본값을 적으면 기본 매개변수(default parameter)가 됩니다.

function buildName(firstName: string, lastName?: string): string {
  return lastName === undefined ? firstName : `${firstName} ${lastName}`;
}

function greet(name: string = "게스트"): string {
  return `안녕하세요, ${name}님!`;
}

console.log(buildName("김"), "/", buildName("이", "철수"));
console.log(greet("김아영"), "/", greet());
김 / 이 철수
안녕하세요, 김아영님! / 안녕하세요, 게스트님!

lastName을 넘기지 않으면 그 값은 undefined이므로 본문에서 따로 확인했고, greet()처럼 name을 넘기지 않으면 기본값 "게스트"가 쓰입니다. 이제 buildName은 인수를 한 개나 두 개 받으므로, buildName("박", "영희", "씨")는 Expected 1-2 arguments, but got 3.으로 진단됩니다. 두 매개변수의 순서 규칙과 기본값이 쓰이는 정확한 조건은 다음 글에서 자세히 봅니다.

나머지 매개변수

넘길 인수의 개수가 정해져 있지 않을 때는 나머지 매개변수(rest parameter)를 씁니다. 매개변수 앞에 ...을 붙이면 남은 인수가 모두 배열로 모이며, 그 배열의 타입을 number[]처럼 적습니다.

function sumAll(...numbers: number[]): number {
  let total = 0;
  for (const value of numbers) {
    total += value;
  }
  return total;
}

console.log(sumAll(1, 2, 3), sumAll(10, 20, 30, 40), sumAll(...[1, 2, 3]));
6 100 6

나머지 매개변수는 매개변수 목록의 맨 끝에 하나만 둘 수 있습니다. 모인 배열의 요소는 모두 number여야 하므로, sumAll(1, 2, "3")은 세 번째 인수에서 TS2345로 진단됩니다. 매개변수를 ...rest: [string, number]처럼 튜플로 적으면 인수마다 다른 타입과 정확한 개수를 정할 수도 있습니다.

같은 ... 기호가 호출하는 쪽에서는 반대의 일을 합니다. 선언의 ...은 여러 인수를 배열로 모으고, 호출의 ...은 배열의 요소를 낱낱의 인수로 펼치는 전개(spread) 구문입니다. 마지막 호출 sumAll(...[1, 2, 3])은 세 숫자를 펼쳐 넘기고, 함수는 그것을 다시 numbers 배열로 모아 6을 계산합니다.

함수 타입 표현

함수도 값이므로 함수 자체의 타입을 적을 수 있습니다. (a: number, b: number) => number는 "숫자 두 개를 받아 숫자를 돌려주는 함수"라는 타입이며, 여기에 별칭을 붙이면 같은 모양의 함수를 여러 곳에서 이름으로 쓸 수 있습니다.

type AddFunction = (a: number, b: number) => number;

const myAdd: AddFunction = function (x, y) {
  return x + y;
};
const myMultiply: AddFunction = (x, y) => x * y;

console.log(myAdd(5, 7), myMultiply(5, 7));
12 35

myAdd와 myMultiply의 매개변수에는 타입을 적지 않았지만, 변수의 타입 AddFunction에서 x와 y가 모두 number로 정해집니다. 2장에서 본 문맥에 의한 타입 결정이며, 매개변수의 이름은 타입에 적은 a, b와 달라도 됩니다. 반환값이 문자열인 함수를 넣으면, 화살표 함수의 본문에서 바로 진단이 나옵니다.

const myInvalidAdd: AddFunction = (x, y) => "결과: " + (x + y);
error TS2322: Type 'string' is not assignable to type 'number'.

함수 타입은 받는 값과 돌려주는 값의 모양만 정하고, 무엇을 계산하는지는 정하지 않습니다. 그래서 이름이 AddFunction인데도 곱하는 myMultiply가 검사를 통과했습니다. 타입이 맞는다고 함수가 의도한 계산을 한다는 보장은 없으므로, 동작은 실행 결과와 테스트로 따로 확인합니다.

더 깊이: 함수끼리 할당할 수 있는 조건

함수를 함수 타입의 변수에 넣을 때, 컴파일러는 매개변수의 개수와 타입, 반환 타입을 따로 비교합니다. 매개변수는 적은 쪽이 많은 쪽에 들어갈 수 있어서, 매개변수가 하나인 함수를 두 개를 받는 함수 타입에 넣어도 통과합니다. 공식 Handbook의 타입 호환성 글은 그 이유로, 자바스크립트에서 남는 인수를 무시하는 일이 흔하다는 점을 듭니다. forEach는 콜백에 요소와 인덱스, 배열 세 개를 넘기지만 요소만 쓰는 콜백을 넘기는 것이 자연스러운 것과 같습니다. 반환 타입은 반대로, 넣는 함수의 반환 타입이 변수에 적은 반환 타입에 할당될 수 있어야 합니다.

매개변수의 타입을 비교하는 규칙은 설정과 문법에 따라 다릅니다. strict에 포함된 strictFunctionTypes가 켜져 있으면, (x: string) => void 함수는 (x: string | number) => void 타입에 들어가지 못합니다. 그 함수는 문자열만 처리할 줄 아는데, 변수의 타입은 숫자를 넘기는 호출도 허용하기 때문입니다. 그런데 이 엄격한 비교는 함수 타입 문법에만 적용되고, func(x: string | number): void처럼 메서드 문법으로 적은 멤버에는 예전의 느슨한 비교가 남아 있습니다. TSConfig 레퍼런스는 그 이유로, 이 기능을 만들면서 DOM을 포함해 본래 안전하지 않은 클래스 계층이 많이 발견되었다는 점을 듭니다. 그러니 콜백을 받는 멤버를 엄격하게 검사하고 싶다면 메서드 문법 대신 onChange: (value: string) => void처럼 속성에 함수 타입을 적습니다.

타입이 지키지 못하는 값의 조건

함수 타입은 값의 종류만 검사하므로, 같은 number라도 업무에 맞지 않는 값은 걸러 내지 못합니다. 다음 subtotal은 상품의 단가와 수량을 받아 금액을 계산하는데, 수량은 0 이상의 정수여야 합니다.

type PricePolicy = (unitPrice: number, quantity: number) => number;

const subtotal: PricePolicy = (unitPrice, quantity) => {
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new RangeError("수량은 0 이상의 정수여야 합니다.");
  }
  return unitPrice * quantity;
};

console.log(subtotal(1200, 3), subtotal(1200, 0));
3600 0

subtotal(1200, 1.5)는 1.5도 number이므로 검사를 통과하지만, 본문의 확인에 걸려 실행 중에 RangeError를 던집니다. 반면 subtotal(1200, "3")은 문자열이 number 매개변수에 맞지 않아 실행 전에 TS2345로 진단됩니다. 이렇게 값의 종류는 타입이, 정수인지와 범위 같은 조건은 본문의 검사가 맡도록 나누어 둡니다.

정리

  • 매개변수 뒤의 : 타입은 받을 값을, 괄호 뒤의 : 타입은 돌려줄 값을 정하며, 함수 선언과 함수 표현식, 화살표 함수에 모두 같은 방식으로 씁니다.
  • 컴파일러는 호출할 때 인수의 타입과 개수를 검사해, 틀린 타입은 TS2345로, 맞지 않는 개수는 TS2554로 진단합니다.
  • ?를 붙인 선택적 매개변수는 생략하면 undefined가 되고, 기본 매개변수는 생략하면 기본값이 쓰입니다.
  • 나머지 매개변수는 남은 인수를 배열로 모으고, 호출의 전개 구문은 배열을 낱낱의 인수로 펼칩니다.
  • (a: number) => number 같은 함수 타입은 받는 값과 돌려주는 값의 모양만 정하므로, 계산이 맞는지와 값의 범위는 실행 결과와 본문의 검사로 확인합니다.

연습 문제

1. 수량과 빈 합계

위의 subtotal에 수량 4와 -1, 문자열 "4"를 각각 넘기면 어떻게 되나요? 그리고 sumAll()처럼 인수를 하나도 넘기지 않으면 무엇을 돌려주나요?

해설 보기

수량 4는 1200×4로 4800을 돌려줍니다. -1은 number라 검사를 통과하지만 0보다 작으므로 본문에서 RangeError를 던지고, 문자열 "4"는 number 매개변수에 맞지 않아 실행 전에 TS2345로 진단됩니다. sumAll()의 numbers는 빈 배열이어서 반복문이 한 번도 돌지 않으므로, 처음 값 0을 그대로 돌려줍니다.

2. 배열을 펼쳐 넘기기

const values = [4, 5];일 때 sumAll(...values, 6)과 add(...values)는 각각 검사를 통과하나요? 통과한다면 결과는 무엇인가요?

해설 보기

sumAll(...values, 6)은 나머지 매개변수가 몇 개의 숫자든 받으므로 통과하고, 4와 5, 6을 더해 15를 돌려줍니다. add(...values)는 values의 타입이 길이를 모르는 number[]이어서, 정확히 두 개를 받는 add에 맞는지 알 수 없으므로 A spread argument must either have a tuple type or be passed to a rest parameter.로 진단됩니다. const pair: [number, number] = [4, 5];처럼 길이를 정한 튜플을 펼치면 add(...pair)는 통과해 9를 돌려줍니다.

3. 같은 함수 타입, 다른 계산

AddFunction 타입의 변수에 (x, y) => x - y를 넣으면 검사를 통과하나요? 통과한다면 이 사실이 AddFunction이라는 이름에 대해 말해 주는 것은 무엇인가요?

해설 보기

통과합니다. 매개변수 두 개가 number이고 x - y도 number이므로, 모양이 AddFunction과 같기 때문입니다. 함수 타입과 그 별칭의 이름은 입력과 출력의 종류만 정할 뿐 계산의 내용을 정하지 않으므로, 더하기를 하는지는 이름이 아니라 실행 결과로 확인해야 합니다.

다음 글 선택적 매개변수와 기본 매개변수에서는 인수를 생략했을 때와 undefined나 null을 넘겼을 때 매개변수에 무엇이 들어가는지, 매개변수의 순서를 어떻게 정해야 하는지 자세히 봅니다.