본문으로 건너뛰기

함수 타입과 콜백

타입스크립트 목차기본 타입과 함수레슨 12 / 51

목차

함수 타입은 함수가 받는 매개변수와 돌려주는 값의 타입을 (n: number) => string⁠처럼 적은 것입니다. 함수를 넘겨받는 자리, 곧 콜백(callback)을 받는 자리에는 이런 함수 타입이 있어서, 검사기는 그 자리에 바로 쓴 콜백의 매개변수 타입을 문맥에서 정합니다.

앞 레슨까지는 매개변수에 타입을 적지 않으면 TS7006이었습니다. 그런데 prices.map((p) => …)처럼 넘기는 자리에 바로 쓴 콜백은 p에 타입을 적지 않아도 검사를 지납니다. 이 레슨은 그 까닭과, 함수를 넘기는 자리에서 검사기가 무엇을 맞춰 보는지 다룹니다.

9단계 중 1단계. 1번 줄: prices는 number[]입니다. 원소 하나하나는 number입니다.

1콜백의 매개변수는 넘기는 자리에서 정해진다

JS 다시 보기: map은 원소마다 콜백을 불러, 돌려받은 값들로 새 배열을 만듭니다(JS 'map·filter·find').

const prices = [4000, 4500];
const labels = prices
  .map((p) => p.toFixed(0));
console.log(labels);
검사 결과 오류 없음
실행 결과
[ '4000', '4500' ]

2~3번 줄은 폰 화면에 맞추려고 점 앞에서 나눈 한 문장입니다. 3번 줄의 p에는 타입을 적지 않았지만 TS7006이 나지 않고, 지금 타입은 number입니다. map이 받는 자리가 'prices의 원소(number)를 첫 인자로 받는 함수'라고 정해져 있어서, 그 자리에 바로 쓴 콜백의 p도 number로 정해집니다. 이렇게 둘레에서 정해지는 것을 문맥에서 추론한다고 합니다(검사 규칙 3). 콜백이 돌려주는 p.toFixed(0)은 글자라서, map이 만든 labels의 지금 타입은 string[]입니다.

검사기가 p를 number로 본다는 것은 number에 없는 메서드를 쓸 때 드러납니다.

const prices = [4000, 4500];
const upper = prices
  .map((p) => p.toUpperCase());
검사 결과 오류 1개
main.ts(3,17): error TS2339: Property 'toUpperCase' does not exist on type 'number'.
실행 결과
main.ts:3  .map((p) => p.toUpperCase());                ^ TypeError: p.toUpperCase is not a function    at main.ts:3:17    at Array.map (<anonymous>)    at main.ts:3:4

TS2339는 'number 타입에는 toUpperCase 속성이 없다'는 뜻입니다. 검사기는 실행하기 전에 이 줄을 막고, Node는 돌리다 3번 줄에서 멈춥니다.

2함수 타입 읽기 — 매개변수와 돌려주는 타입

JS 다시 보기: 화살표 함수도 값이라 이름을 붙이거나 다른 함수에 넘길 수 있습니다(JS '화살표 함수와 함수 값').

함수도 값이라 타입이 있습니다. 화살표 함수 (n: number) => n + '원'⁠의 타입은 (n: number) => string⁠이고, 'number 하나를 받아 string을 돌려주는 함수'라고 읽습니다. => 왼쪽이 매개변수의 차례와 타입, 오른쪽이 돌려주는 타입입니다.

함수 타입은 길어서 이름을 붙여 쓰는 일이 많습니다. type 이름 = 타입;은 타입에 이름을 붙이는 문장이고, 타입 이름은 대문자로 시작하는 것이 관례입니다. 아래 show는 둘째 매개변수 format으로 '글자로 바꾸는 함수'를 받습니다.

type Format =
  (n: number) => string;
function show(
  price: number,
  format: Format,
) {
  console.log(format(price));
}
show(4500, (n) => n + '원');
show(4500, (n) => n * 2);
검사 결과 오류 1개
main.ts(10,19): error TS2322: Type 'number' is not assignable to type 'string'.
실행 결과
4500원
9000

1~2번 줄은 함수 타입 (n: number) => string⁠에 이름 Format을 붙입니다. format 자리의 타입이 Format이라, 9 · 10번 줄 콜백의 n도 문맥에서 number로 정해집니다. 10번 줄은 콜백이 돌려주는 n * 2가 수라서, Format의 돌려주는 타입 string 자리에 들지 않습니다(검사 규칙 2). 그래서 TS2322가 콜백의 몸 n * 2를 짚습니다. Node는 검사하지 않으니 둘 다 돌려, 둘째 줄에 9000이 찍힙니다.

3함수를 넘길 때는 이름만 쓴다

JS 다시 보기: 함수를 넘길 때 괄호를 붙이면 그 자리에서 불려, 함수가 아니라 돌려준 값이 넘어갑니다(JS '함수를 넘기기').

따로 만든 함수도 map에 넘길 수 있습니다. 아래 label의 타입 (n: number) => string⁠이 map의 콜백 자리에 맞아서, 5번 줄은 검사를 지납니다. 6번 줄은 검사를 지나갈까요? Node로 돌리면 어떻게 될까요? 답을 정한 뒤 펼쳐 보세요.

function label(n: number) {
  return n + '원';
}
const prices = [4000, 4500];
console.log(prices.map(label));
prices.map(label());
검사 결과와 실행 결과 보기
검사 결과 오류 2개
main.ts(6,12): error TS2554: Expected 1 arguments, but got 0.main.ts(6,12): error TS2345: Argument of type 'string' is not assignable to parameter of type '(value: number, index: number, array: number[]) => unknown'.
실행 결과
[ '4000원', '4500원' ]main.ts:6prices.map(label());       ^ TypeError: string "undefined원" is not a function    at Array.map (<anonymous>)    at main.ts:6:8

예상과 다르면 그곳이 배울 곳입니다. 6번 줄에 오류가 둘입니다. label()은 그 자리에서 label을 불러 글자 하나를 돌려주고, map에는 함수가 아니라 그 글자가 넘어갑니다. TS2554는 label을 인자 없이 불렀다는 것, TS2345는 글자를 콜백 자리에 넣을 수 없다는 것입니다. 콜백 자리의 타입이 '원소 · 번호 · 배열을 받는 함수'로 찍혀 있습니다(끝의 unknown은 '어떤 값이든' — 6단원). Node는 5번 줄을 찍은 뒤 6번 줄에서 멈춥니다. 끝의 오류 문장은 '글자 "undefined원"은 함수가 아니다'라는 뜻입니다.

함수를 넘길 때는 이름만 씁니다. 괄호를 붙이는 것은 그 자리에서 부를 때입니다.

예제

오늘 가격 가운데 4000원보다 싼 것만 골라 '원'을 붙여 찍습니다.

function label(n: number) {
  return n + '원';
}
const prices = [4500, 2000];
const cheap = prices
  .filter((p) => p < 4000);
console.log(cheap.map(label));
검사 결과 오류 없음
실행 결과
[ '2000원' ]

6번 줄 콜백의 p는 문맥에서 number이고, filter는 콜백이 참을 돌려준 원소만 남기니 cheap은 number[]입니다. 7번 줄은 label이 number를 받아 string을 돌려주는 함수라 map의 콜백 자리에 맞고, 결과는 string[]입니다. 매개변수 타입을 적은 곳은 따로 만든 label 하나뿐입니다.

자주 틀리는 곳

틀린 생각: 콜백으로 넘길 함수라면 따로 만든 함수의 매개변수도 문맥에서 추론된다.

넘기는 자리에 바로 쓴 콜백은 매개변수 타입을 적지 않아도 되니, 넘길 함수는 다 그럴 것 같습니다. 그러나 따로 만든 함수는 만드는 줄에서 타입이 정해집니다.

const prices = [4000, 4500];
const a = prices
  .map((p) => p + 1);
function plus(n) {
  return n + 1;
}
const b = prices.map(plus);
검사 결과 오류 1개
main.ts(4,15): error TS7006: Parameter 'n' implicitly has an 'any' type.

3번 줄의 p는 문맥에서 number로 정해져 오류가 없고, 4번 줄의 n은 TS7006입니다. 검사기는 4~6번 줄의 plus를 읽을 때 그 함수가 어디로 넘겨질지 보지 않습니다. 거꾸로, 3번 줄처럼 바로 쓴 콜백에 (p: number)를 적는 것은 틀리지 않지만 하지 않아도 되는 일입니다.

맞는 규칙: 문맥에서 추론되는 것은 넘기는 자리에 바로 쓴 콜백의 매개변수뿐입니다. 따로 만든 함수의 매개변수에는 타입을 적습니다(검사 규칙 3).

틀린 생각: 콜백도 넘기는 자리가 주는 인자를 다 받아야 한다.

forEach의 콜백 자리도 원소 · 번호 · 배열 셋을 주는 함수 타입이라, 콜백도 셋을 다 받아야 할 것 같습니다. 그러나 하나만 받는 콜백도 검사를 지납니다.

const prices = [4000, 4500];
prices.forEach((p) => {
  console.log(p);
});
검사 결과 오류 없음
실행 결과
4000
4500

forEach는 콜백을 부를 때 인자 셋을 넘기고, p는 그 가운데 첫째를 받습니다. 받지 않은 인자는 버려집니다. 막히는 쪽은 거꾸로입니다. 함수의 입력과 출력의 cost(2)처럼, 부르는 쪽이 필요한 인자를 덜 주면 TS2554입니다.

맞는 규칙: 매개변수를 덜 받는 함수는 인자를 더 주는 자리에 들어갑니다. 남는 인자는 버려지기 때문이고, 막히는 것은 부를 때 필요한 인자를 덜 주는 쪽입니다(검사 규칙 2).

틀린 생각: 함수 타입에 적은 매개변수 이름과 넣는 함수의 매개변수 이름이 같아야 한다.

Format에 (n: number)라고 적었으니, 그 자리에 넣는 함수의 매개변수도 n이어야 할 것 같습니다. 그러나 검사기는 이름을 견주지 않습니다.

type Format =
  (n: number) => string;
const won: Format =
  (price) => price + '원';
console.log(won(4000));
검사 결과 오류 없음
실행 결과
4000원

4번 줄 매개변수 이름은 price지만 검사를 지납니다. price도 문맥에서 number로 정해집니다.

맞는 규칙: 함수 타입은 매개변수의 차례와 타입, 돌려주는 타입만 봅니다. 매개변수 이름은 그 함수 안에서 부르는 이름일 뿐입니다(검사 규칙 2).

확인 문제

1. 1절 첫 코드의 3번 줄을 .map((p) => p > 4000);⁠으로 바꿨습니다. 2번 줄에서 labels의 지금 타입은 무엇일까요? Node로 돌리면 무엇이 찍힐까요?

const prices = [4000, 4500];
const labels = prices
  .map((p) => p > 4000);
console.log(labels);
힌트

map이 만드는 배열의 원소는 콜백이 돌려준 값입니다. 3번 줄 콜백이 돌려주는 p > 4000이 무슨 타입인지 봅니다.

정답

labels의 지금 타입은 boolean[]입니다.

실행 결과
[ false, true ]

콜백이 돌려주는 p > 4000은 참·거짓이라, map이 만든 배열은 boolean[]입니다. 4000은 4000보다 크지 않아 false, 4500은 true입니다.

흔한 오답은 number[]입니다. map이 원래 배열과 같은 원소 타입의 배열을 만든다고 본 것입니다. 새 배열의 원소 타입은 콜백이 돌려주는 타입으로 정해집니다.

2. 검사를 지나도록 2번 줄의 빈칸에 함수 타입을 쓰세요. Check는 수 하나를 받아 참·거짓을 돌려주는 함수의 타입입니다.

type Check =
  ____;
const cheap: Check =
  (n) => n < 4000;
console.log(cheap(3500));
힌트

=> 왼쪽에 매개변수와 그 타입을, 오른쪽에 돌려주는 타입을 씁니다. 4번 줄 n < 4000이 무슨 타입인지 봅니다.

정답

빈칸은 (n: number) => boolean⁠입니다.

type Check =
  (n: number) => boolean;
const cheap: Check =
  (n) => n < 4000;
console.log(cheap(3500));
검사 결과 오류 없음
실행 결과
true

다르게 써도 되는 곳: 매개변수 이름은 무엇이어도 됩니다. (price: number) => boolean⁠도 같은 타입입니다(자주 틀리는 곳 셋째).

흔한 오답은 (n: number) => number⁠입니다. 받는 값과 돌려주는 값을 같은 타입으로 본 것입니다. 그러면 4번 줄 n < 4000이 number 자리에 들지 않아 TS2322로 막힙니다.

3. 성적에서 80점 이상을 고르는 코드입니다. 3번 줄 passed와 5번 줄 marks의 지금 타입을 쓰고, Node로 돌리면 무엇이 찍히는지 마지막 줄까지 쓰세요.

const scores = [80, 95, 70];
const passed = scores
  .filter((s) => s >= 80);
const marks = scores
  .map((s) => s >= 80);
console.log(passed);
console.log(marks);
힌트

filter는 원래 원소 가운데 몇을 남기고, map은 콜백이 돌려준 값으로 새 배열을 만듭니다. 두 콜백이 돌려주는 값은 같습니다.

정답

passed는 number[], marks는 boolean[]입니다.

실행 결과
[ 80, 95 ]
[ true, true, false ]

filter는 콜백이 참을 돌려준 원소만 남기므로 원소 타입이 그대로 number입니다. map은 콜백이 돌려준 참·거짓으로 새 배열을 만듭니다.

흔한 오답은 passed를 boolean[]로 쓰는 것입니다. filter도 콜백이 돌려준 값을 담는다고 본 것입니다. 콜백 모양이 같아도 받는 함수가 하는 일이 다릅니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 3절 코드를 main.ts로 저장해 npx tsc로 검사하고 node main.ts로 돌립니다. 그다음 6번 줄의 label()을 label로 바꾸면 두 결과가 어떻게 바뀔지 먼저 적고, 다시 검사하고 돌려서 맞춰 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로