매개변수와 반환값 타이핑
자판기를 상상해봅시다. 동전을 넣으면 음료가 나옵니다. 동전 자리에 카드를 꽂으려 하면 기계가 거부합니다. 출구로 나오는 것도 항상 음료입니다. 영수증이나 동전이 나온다면 뭔가 잘못된 것입니다.
TypeScript의 함수도 똑같습니다. "이 자리에는 숫자만 넣을 수 있다", "결과는 항상 문자열이다"라고 선언합니다. 규칙을 어기면 컴파일러가 바로 알려줍니다.
함수 선언문에 타입 붙이기
JavaScript 함수에 타입 정보를 추가하는 방법은 간단합니다. 각 매개변수 이름 뒤에 : 타입을 붙이고, 닫는 괄호 뒤에 반환 타입을 씁니다.
// 새 파일: greet.tsfunction greet(name: string): string { return `안녕하세요, ${name}님!`;}console.log(greet("이민준")); // 안녕하세요, 이민준님!console.log(greet(42)); // 오류: 'number' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다.
greet 함수는 string만 받고 string만 돌려줍니다. 숫자를 넘기면 컴파일 오류가 납니다. JavaScript였다면 "안녕하세요, 42님!"이 출력됐겠지만, TypeScript는 이 실수를 실행 전에 막아줍니다.
여러 매개변수 타이핑
매개변수가 여럿이어도 방식은 같습니다.
// 새 파일: calculator.tsfunction add(a: number, b: number): number { return a + b;}function describe(name: string, age: number, isStudent: boolean): string { const status = isStudent ? "학생" : "직장인"; return `${name}(${age}세)은 ${status}입니다.`;}console.log(add(10, 20)); // 30console.log(describe("박지수", 21, true)); // 박지수(21세)은 학생입니다.console.log(describe("이민준", 28, false)); // 이민준(28세)은 직장인입니다.
반환 타입 void
반환값이 없는 함수는 반환 타입을 void로 선언합니다. "아무것도 돌려주지 않겠다"는 선언입니다.
// 새 파일: logger.tsfunction logMessage(message: string): void { console.log(`[LOG] ${message}`); // return 없음, 또는 return; 만 가능}function printSeparator(): void { console.log("─".repeat(40));}logMessage("서버 시작");printSeparator();logMessage("요청 수신");
void를 선언한 함수에서 값을 반환하려 하면 오류가 납니다.
function wrongVoid(): void { return "hello"; // 오류: 'void' 함수에서 'string'을 반환할 수 없습니다.}
반환 타입 추론
사실 TypeScript는 함수 본문을 보고 반환 타입을 스스로 추론할 수 있습니다.
// 새 파일: inference.tsfunction multiply(a: number, b: number) { return a * b; // TypeScript가 반환 타입을 number로 추론합니다}function getFullName(first: string, last: string) { return `${first} ${last}`; // string으로 추론합니다}const result = multiply(3, 4); // result의 타입은 numberconst name = getFullName("이", "민준"); // name의 타입은 string
추론이 잘 작동하는데 왜 반환 타입을 명시할까요? 두 가지 이유가 있습니다.
첫째, 의도를 명확히 합니다. 함수를 호출하는 사람이 반환 타입을 바로 알 수 있습니다.
둘째, 실수를 방지합니다. "반드시 string을 반환해야 한다"고 선언했는데 어떤 코드 경로에서 number를 반환하면, 추론에 맡겼을 때는 오류 없이 넘어가지만 명시했을 때는 바로 오류가 납니다.
// 반환 타입 명시로 실수를 방지하는 예function getUserName(id: number): string { if (id === 1) { return "이민준"; } // 오류: 함수의 일부 코드 경로가 값을 반환하지 않습니다 // (반환 타입을 명시했기 때문에 컴파일러가 잡아줍니다)}
함수 표현식 타이핑
함수를 변수에 담는 표현식 방식도 동일하게 타입을 붙입니다.
// 새 파일: function-expression.tsconst square = function(n: number): number { return n * n;};const greet = function(name: string): string { return `안녕하세요, ${name}님!`;};console.log(square(5)); // 25console.log(greet("박지수")); // 안녕하세요, 박지수님!
변수 자체에 함수 타입을 먼저 선언하고 나중에 함수를 대입할 수도 있습니다.
// 변수에 함수 타입을 먼저 선언let calculator: (a: number, b: number) => number;// 나중에 함수 구현을 대입calculator = function(a, b) { return a + b; // 매개변수 타입은 위에서 선언했으므로 생략 가능};console.log(calculator(10, 20)); // 30// 타입이 다른 함수를 대입하면 오류calculator = function(a: string, b: string) { // 오류 return a + b;};
(a: number, b: number) => number가 함수 타입 표현식입니다. "숫자 두 개를 받아 숫자를 반환하는 함수"라는 의미입니다.
타입으로 선언된 매개변수 활용하기
PART 03에서 배운 인터페이스와 유니온 타입을 매개변수에 그대로 쓸 수 있습니다.
// 새 파일: typed-params.tsinterface User { name: string; age: number; email: string;}function printUserInfo(user: User): void { console.log(`이름: ${user.name}`); console.log(`나이: ${user.age}세`); console.log(`이메일: ${user.email}`);}function formatId(id: number | string): string { if (typeof id === "number") { return `ID-${id.toString().padStart(6, "0")}`; } return `ID-${id.toUpperCase()}`;}const user: User = { name: "이민준", age: 22, email: "[email protected]"};printUserInfo(user);console.log(formatId(42)); // ID-000042console.log(formatId("abc")); // ID-ABC
함수가 기대하는 구조를 인터페이스로 명시하면, 잘못된 객체를 넘기는 실수를 컴파일러가 바로 잡아줍니다.
never 타입 — 절대 반환하지 않는 함수
void와 비슷해 보이지만 다른 타입이 있습니다. never는 "이 함수는 절대 정상적으로 종료되지 않는다"는 의미입니다. 예외를 항상 던지거나 무한 루프인 함수가 해당됩니다.
// 새 파일: never.tsfunction throwError(message: string): never { throw new Error(message); // 이 줄 이후 코드는 절대 실행되지 않습니다}function infiniteLoop(): never { while (true) { // 영원히 실행 }}// 실용적인 활용: 도달할 수 없는 경우 처리function assertUnreachable(value: never): never { throw new Error(`처리되지 않은 값: ${value}`);}
never는 직접 쓸 일이 많진 않지만, 모든 경우를 빠짐없이 처리했는지 컴파일러가 검증할 때 유용합니다.
함수의 입출구에 타입을 붙이면 "계약서"가 생깁니다. 이 함수를 호출하는 모든 코드가 계약을 지켜야 합니다. 지키지 않으면 컴파일러가 알려줍니다.
다음 장에서는 조금 더 유연한 함수를 만드는 방법을 배웁니다. 넣어도 되고 안 넣어도 되는 매개변수, 미리 설정된 기본값, 그리고 개수가 정해지지 않은 매개변수까지 다룹니다.