타입스크립트가 값을 지켜주는 건 좋은데, 함수 앞에선 이야기가 조금 다르다. 함수는 값이 드나드는 문이라, 매개변수로 뭘 받고 무엇을 돌려주는지 양쪽을 다 챙겨야 한다. 자바스크립트는 인자를 잘못 넘겨도 조용히 실행하고 엉뚱한 결과를 뱉었는데, 그 사고가 대부분 함수 경계에서 났다. 여기서는 함수의 입구와 출구에 타입을 붙이는 법을 훑는다. 아래 결과는 전부 실제 타입 검사기를 돌려 나온 그대로다.


매개변수엔 타입을 직접 붙여야 한다. 변수는 값만 보고 타입을 알아서 정해주지만(추론), 함수 매개변수는 다르다. 아직 아무 값도 안 들어온 자리라 타입스크립트가 추론할 근거가 없다.


function greet(name) {
return "hi " + name;
}


error TS7006: Parameter 'name' implicitly has an 'any' type.


엄격 모드(strict, 실무의 표준 설정)에선 타입을 안 붙인 매개변수를 any로 몰래 넘어가게 두지 않고 이렇게 경고한다. name: string이라고 적어주면 사라진다. 함수 매개변수는 타입 표기를 붙여야 하는 대표적인 자리라고 기억해두면 된다.


반환 타입은 대개 추론된다. 반면 함수가 무엇을 돌려주는지는 return 뒤 값을 보면 알 수 있어서, 보통 안 적어도 알아서 정해진다.


function add(a: number, b: number) {
return a + b;
}
const r = add(1, 2); // r의 타입: number


숫자 둘을 더해 돌려주니 반환 타입이 number로 정해지고, 받은 r도 자동으로 number가 된다. 다만 함수가 복잡하거나, 내가 의도한 반환 타입을 못박아 실수를 막고 싶으면 콜론으로 적어줄 수 있다. function add(a: number, b: number): number라고 쓰면, 실수로 문자열을 돌려주는 순간 그 함수 안에서 바로 걸린다.


물음표 매개변수와 기본값으로 생략을 허용한다. 매개변수 이름 뒤에 ?를 붙이면 넘겨도 되고 안 넘겨도 되는 선택 인자가 된다. 아예 기본값을 정해두는 방법도 있다.


function log(msg: string, tag?: string) {
return tag ? tag + msg : msg;
}
log("hello"); // ok, tag 생략
log("hello", "INFO"); // ok
log("hello", 1);


error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.


tag는 생략해도 되지만, 넘길 거라면 문자열이어야 한다. 그래서 마지막 줄의 숫자 1은 걸린다. 매번 같은 값이 기본이라면 function box(w: number = 10)처럼 등호로 기본값을 줘도 된다. 이러면 box()w10이 되고, box("x")처럼 엉뚱한 타입을 넣으면 역시 막힌다.


함수 자체도 하나의 타입이다. 함수를 다른 함수에 넘길 때(콜백), 그 자리에 "이런 매개변수를 받아 이런 걸 돌려주는 함수"라고 모양을 적는다. (x: number) => string이 그 표기다.


function run(fn: (x: number) => string) {
return fn(3);
}
run((x) => x.toFixed(1)); // ok
run((x) => x);


error TS2322: Type 'number' is not assignable to type 'string'.


run은 "숫자를 받아 문자열을 주는 함수"를 요구한다. 첫 호출은 toFixed로 문자열을 돌려주니 통과하지만, 둘째는 받은 숫자를 그대로 돌려줘 문자열이 아니라 걸린다. 눈여겨볼 건 넘긴 화살표 함수의 x에 타입을 안 적었는데도 number로 정해졌다는 점이다. 받는 쪽이 이미 모양을 정해뒀으니, 넣는 함수의 매개변수 타입은 거기서 흘러 들어온다(문맥 추론).


타입 별칭으로 함수 시그니처에 이름을 붙인다. 같은 함수 모양을 여기저기서 쓸 거라면, 매번 (a: number, b: number) => number를 반복하는 대신 이름을 붙여둔다.


type Reducer = (acc: number, cur: number) => number;
const add: Reducer = (a, b) => a + b; // ok
const bad: Reducer = (a, b) => "x";


error TS2322: Type 'string' is not assignable to type 'number'.


Reducer라는 이름 하나로 함수 모양을 재사용한다. bad는 숫자를 줘야 하는데 문자열을 돌려줘 걸렸다. 매개변수 a, b에 타입을 안 적었는데도 Reducer가 정해둔 대로 둘 다 number로 채워지는 것도 앞과 같은 문맥 추론이다.


void는 아무것도 안 돌려준다는 뜻이다. 반환값을 쓰지 않는 함수의 반환 타입은 void다. 화면에 찍기만 하거나 이벤트를 처리하고 끝나는 함수가 그렇다.


function onClick(handler: () => void) {
handler();
}
onClick(() => 42); // 에러가 아니다


여기서 초보가 갸웃한다. void를 요구했는데 42를 돌려주는 함수를 넘겨도 통과한다. 이건 버그가 아니라 일부러 그렇게 만든 규칙이다. "반환값을 안 쓰겠다"는 자리에는 뭔가를 돌려주는 함수를 넣어도 그 값을 그냥 무시할 뿐이라 안전하기 때문이다. 덕분에 arr.forEach(n => arr.push(n))처럼 반환값이 있는 함수도 콜백으로 편히 넘길 수 있다. void는 "반드시 아무것도 반환하지 마라"가 아니라 "반환값을 신경 쓰지 않겠다"에 가깝다.


나머지 매개변수는 개수 제한 없이 받는다. 인자를 몇 개 받을지 모를 때 ...을 붙이면, 넘어온 인자들이 배열로 묶인다. 자바스크립트의 나머지 매개변수에 타입만 얹은 것이다.


function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // ok
sum(1, "2");


error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.


numsnumber[]라 몇 개를 넘기든 다 숫자여야 한다. 그래서 "2"가 섞이자 그 자리에서 걸린다. 개수는 자유롭게, 대신 타입은 하나로 못박는 셈이다. 나머지 매개변수는 반드시 목록의 맨 끝에 하나만 올 수 있는데, 중간에 두면 어디까지가 앞 인자이고 어디부터가 나머지인지 구분이 안 되기 때문이다.


함수는 값이 오가는 길목이라, 입구인 매개변수와 출구인 반환값 양쪽에 타입을 걸어두면 잘못된 인자가 애초에 함수 안으로 못 들어온다. 매개변수는 붙여줘야 하고 반환은 대개 맡겨도 되며, 함수 자체도 타입이라 콜백으로 주고받을 수 있다는 것, 이 셋만 손에 익으면 남의 코드에서 만나는 화살표 타입 표기가 더는 낯설지 않다.