Chapter 03

함수

TypeScript 함수의 타입 시스템 완전 이해


← 목차로 돌아가기

함수 타입 어노테이션 (Type Annotation)

매개변수 (Parameter, 함수에 전달하는 입력값)와 반환 타입 (Type)을 명시적으로 지정합니다.

// 매개변수 타입 + 반환 타입
function add(a: number, b: number): number {
  return a + b;
}

// 반환 타입 추론 (명시하지 않아도 OK)
function multiply(a: number, b: number) {
  return a * b;  // number로 자동 추론
}

// 문자열을 반환하는 함수
function greet(name: string): string {
  return `안녕하세요, ${name}님!`;
}

// 타입 에러 예시
add("1", 2);         // Error! string은 number에 할당 불가
add(1, 2, 3);        // Error! 매개변수 2개만 허용
greet(42);           // Error! number는 string에 할당 불가
Tip: 매개변수 타입은 반드시 명시하세요. 반환 타입은 추론에 맡겨도 괜찮습니다.

화살표 함수와 타입

// 기본 화살표 함수: (매개변수): 반환타입 => { 본문 }
const add = (a: number, b: number): number => {
  return a + b;
};

// 한 줄 표현식 (중괄호와 return 생략 가능)
const double = (n: number): number => n * 2;

// 함수 타입을 변수에 먼저 선언하고, 나중에 구현
const greet: (name: string) => string = (name) => {
  return `Hello, ${name}!`;
};

// 타입 별칭 (Type Alias)으로 함수 타입 정의 (권장)
type MathOperation = (a: number, b: number) => number;

const subtract: MathOperation = (a, b) => a - b;
const divide: MathOperation = (a, b) => a / b;

// 함수 배열도 가능
const operations: MathOperation[] = [add, subtract, divide];
operations.forEach(op => console.log(op(10, 3)));

선택적 매개변수

// ? 를 붙이면 선택적 매개변수 (호출 시 생략 가능)
function greet(name: string, greeting?: string): string {
  // greeting의 타입: string | undefined (값이 없을 수 있음)
  return `${greeting || "안녕하세요"}, ${name}님!`;
}

greet("홍길동");              // "안녕하세요, 홍길동님!"
greet("홍길동", "반갑습니다");  // "반갑습니다, 홍길동님!"

// 여러 선택적 매개변수
function createUser(
  name: string,
  age?: number,
  email?: string
): void {
  console.log(name);
  if (age !== undefined) console.log(`나이: ${age}`);
  if (email !== undefined) console.log(`이메일: ${email}`);
}
규칙: 선택적 매개변수는 반드시 필수 매개변수 뒤에 와야 합니다. function bad(x?: number, y: number)는 에러입니다.

기본값 매개변수

// 기본값이 있으면 타입을 추론하고, 생략 가능
function greet(name: string, greeting: string = "안녕하세요"): string {
  return `${greeting}, ${name}님!`;
}

greet("홍길동");              // "안녕하세요, 홍길동님!"
greet("홍길동", "환영합니다"); // "환영합니다, 홍길동님!"

// 기본값에서 타입 추론
function createConfig(
  port: number = 3000,
  host: string = "localhost",
  debug: boolean = false
) {
  return { port, host, debug };
}

// 기본값은 중간에도 올 수 있음 (undefined로 건너뛰기)
function format(
  value: number,
  prefix: string = "$",
  suffix: string = ""
): string {
  return `${prefix}${value}${suffix}`;
}

format(100);                   // "$100"
format(100, undefined, "원");  // "$100원"
format(100, "\\", "");          // "\\100"

나머지 매개변수 (Rest Parameters)

// ...args로 가변 인수 받기 (개수 제한 없이 여러 개를 배열로 받음)
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0); // 모든 숫자를 더함
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15
sum();                   // 0

// 일반 매개변수 + 나머지 매개변수
function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, ...messages);
}

log("INFO", "서버 시작", "포트: 3000");
log("ERROR", "연결 실패");

// 튜플과 함께 사용
function createUser(
  name: string,
  ...info: [age: number, email: string]
): void {
  const [age, email] = info;
  console.log(`${name}, ${age}, ${email}`);
}

createUser("Kim", 25, "kim@example.com");

함수 오버로드 (Overload)

같은 함수명으로 다른 매개변수 타입에 따라 다른 반환 타입을 제공합니다.
하나의 함수가 여러 가지 "사용 설명서"를 가지는 것과 같습니다.

// 오버로드 시그니처 (Signature, 함수의 입출력 형태 선언)
function createElement(tag: "a"): HTMLAnchorElement;
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement;

// 구현부 (실제 로직)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

// 호출 시 반환 타입이 정확하게 추론
const link = createElement("a");       // HTMLAnchorElement
const canvas = createElement("canvas"); // HTMLCanvasElement
const div = createElement("div");       // HTMLElement

link.href = "https://example.com";  // OK (HTMLAnchorElement)
canvas.getContext("2d");             // OK (HTMLCanvasElement)

함수 오버로드 실전 예제

// 입력 타입에 따라 반환 타입이 달라지는 경우
function parseValue(value: string): number;
function parseValue(value: string[]): number[];
function parseValue(value: string | string[]): number | number[] {
  if (Array.isArray(value)) {
    return value.map(v => parseInt(v, 10));
  }
  return parseInt(value, 10);
}

const single = parseValue("42");          // number
const multiple = parseValue(["1", "2"]);  // number[]

// 매개변수 개수가 다른 오버로드
function formatDate(timestamp: number): string;
function formatDate(year: number, month: number, day: number): string;
function formatDate(yearOrTimestamp: number, month?: number, day?: number): string {
  if (month !== undefined && day !== undefined) {
    return `${yearOrTimestamp}-${month}-${day}`;
  }
  return new Date(yearOrTimestamp).toISOString();
}

formatDate(1700000000000);    // "2023-11-14T..."
formatDate(2024, 1, 15);     // "2024-1-15"

void vs undefined 반환

void

// void: "반환값을 사용하지 않겠다"
function logMessage(msg: string): void {
  console.log(msg);
}

// 콜백에서의 void는 특별함
type Callback = () => void;

// 반환값이 있어도 무시됨!
const cb: Callback = () => {
  return 42; // OK! void 콜백이라 반환값 무시
};

// void 콜백의 반환값은 사용 불가
const result: void = cb();
// result를 숫자처럼 사용할 수 없음

undefined

// undefined: 정확히 undefined를 반환
function nothing(): undefined {
  return undefined; // 반드시 명시
}

// return 없으면 에러
function bad(): undefined {
  // Error! 반환문이 필요
}

// 실제 차이점 예시
const arr = [1, 2, 3];

// forEach 콜백은 void 반환
arr.forEach((n): void => {
  console.log(n);
  // return 있어도 OK
});

// map에서는 반환 타입이 중요
arr.map((n): number => {
  return n * 2;
});

콜백 (Callback) 함수 타입

콜백이란 "나중에 호출해줘"라고 다른 함수에 전달하는 함수입니다. 특정 작업이 끝난 후 실행됩니다.

// 콜백 타입 정의
type SuccessCallback = (data: string) => void;
type ErrorCallback = (error: Error) => void;

function fetchData(
  url: string,
  onSuccess: SuccessCallback,
  onError: ErrorCallback
): void {
  try {
    // ... 데이터 가져오기
    onSuccess("데이터 로드 완료!");
  } catch (e) {
    onError(e as Error);
  }
}

// 사용 예시
fetchData(
  "/api/users",
  (data) => console.log(data),      // data: string
  (error) => console.error(error)    // error: Error
);

// 이벤트 핸들러 타입
type EventHandler = (event: { type: string; target: HTMLElement }) => void;

const handleClick: EventHandler = (event) => {
  console.log(`${event.type} on ${event.target.tagName}`);
};

고차 함수 (Higher Order Function) 타입

함수를 매개변수로 받거나 반환하는 함수입니다.
마치 "함수를 만들어주는 공장"처럼, 함수 자체를 값으로 다룹니다.

// 함수를 반환하는 함수
function createMultiplier(factor: number): (n: number) => number {
  return (n: number) => n * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

double(5);  // 10
triple(5);  // 15

// 함수를 매개변수로 받는 함수
function applyToArray<T, U>(
  arr: T[],
  fn: (item: T, index: number) => U
): U[] {
  return arr.map(fn);
}

const numbers = [1, 2, 3, 4];
const strings = applyToArray(numbers, (n) => n.toString());
// strings: string[] = ["1", "2", "3", "4"]

const doubled = applyToArray(numbers, (n) => n * 2);
// doubled: number[] = [2, 4, 6, 8]

제네릭 (Generic) 함수 입문

핵심 개념

제네릭은 타입을 매개변수로 받아, 다양한 타입에서 재사용 가능한 함수를 만듭니다.
비유하면, "어떤 타입이든 담을 수 있는 만능 상자"를 만드는 것과 같습니다.

// 문제: 모든 타입에 대해 동일한 로직을 반복해야 함
function firstString(arr: string[]): string | undefined {
  return arr[0];
}
function firstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// 해결: 제네릭으로 한 번만 작성 (<T>가 "타입 매개변수")
function first<T>(arr: T[]): T | undefined {
  return arr[0]; // T가 어떤 타입이든 첫 번째 요소 반환
}

// 타입이 자동 추론됨 (전달한 배열의 타입을 보고 T를 결정)
const a = first(["hello", "world"]);  // T=string -> string | undefined
const b = first([1, 2, 3]);           // T=number -> number | undefined

// 명시적으로 타입 지정도 가능
const c = first<boolean>([true, false]); // T=boolean -> boolean | undefined

제네릭 함수 활용

// 여러 타입 매개변수
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const p = pair("hello", 42);  // [string, number]

// 제네릭 + 제약 조건
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Kim", age: 25 };
const name = getProperty(user, "name");  // string
const age = getProperty(user, "age");    // number
// getProperty(user, "email");  // Error! "email"은 keyof User가 아님

// 제네릭 유틸리티 함수
function toArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value];
}

toArray("hello");          // string[]
toArray([1, 2, 3]);        // number[]

함수 타입 실전 패턴

// 패턴 1: 옵션 객체 매개변수 (설정값을 객체로 묶어서 전달)
interface FetchOptions {
  method?: "GET" | "POST" | "PUT" | "DELETE"; // HTTP 요청 방식
  headers?: Record<string, string>;  // 요청 헤더 (Record: 키-값 쌍의 객체 타입)
  body?: string;     // 요청 본문
  timeout?: number;  // 제한 시간 (밀리초)

function fetchAPI(url: string, options: FetchOptions = {}): Promise<Response> {
  const { method = "GET", timeout = 5000 } = options;
  return fetch(url, { ...options, method });
}

// 패턴 2: 빌더 패턴의 체이닝
function createQueryBuilder() {
  let query = "";
  return {
    select(fields: string[]) { query += `SELECT ${fields.join(", ")} `; return this; },
    from(table: string) { query += `FROM ${table} `; return this; },
    where(condition: string) { query += `WHERE ${condition} `; return this; },
    build(): string { return query.trim(); }
  };
}

const sql = createQueryBuilder()
  .select(["name", "age"])
  .from("users")
  .where("age > 18")
  .build();

Chapter 3 정리

개념 문법 설명
타입 어노테이션(a: number): string매개변수 + 반환 타입 명시
화살표 함수(n: number) => n * 2간결한 함수 표현식
선택적 매개변수name?: string생략 가능 (| undefined)
기본값port = 3000생략 시 기본값 사용
나머지 매개변수...args: number[]가변 인수
오버로드여러 시그니처 선언입력에 따라 반환 타입 변경
void(): void반환값 사용하지 않음
콜백 타입type Fn = (x: T) => R함수 타입 별칭
제네릭<T>(arr: T[]): T타입을 매개변수로
Next

수고하셨습니다!


다음 챕터: 인터페이스 & 타입 별칭 →


← 목차로 돌아가기