[TS] 함수와 타입



01


함수 타입 정의

타입스크립트에서 함수 타입을 정의하려면 함수가 받는 매개변수의 타입과 반환값의 타입을 정의해 주면 돼요.

 

예를 들어, 두 개의 숫자를 더하는 함수는 다음처럼 정의할 수 있어요.

여기서 반환값의 타입은 명시적으로 직접 지정해주거나 타입스크립트가 자동으로 추론해서 number로 지정해 줍니다.

function add(a: number, b: number) {
  return a + b;
}
const add = (a: number, b: number): number => a + b;

 

 

✅ 기본값

함수의 매개변수에 기본값을 설정할 수 있어요. 기본값을 설정하면, 인자가 주어지지 않았을 때 기본값이 사용돼요.

function greet(name: string = "Guest") {
  console.log("Hello, " + name);
}

이 함수에서 name이 없으면 "Guest"라는 기본값이 사용됩니다.

 

 

✅ 선택적 매개변수:?

선택적 매개변수는?를 사용해서 정의합니다. 선택적 매개변수는 인자가 주어지지 않아도 에러가 나지 않아요.

function greet(name?: string) {
  console.log("Hello, " + (name || "Guest"));
}

 

단, 필수 매개변수와 선택적 매개변수를 같이 사용하는 경우에는 필수 매개변수는 선택적 매개변수 앞에 위치해야 합니다.

// ✅
function greet(id: number, name?: string) {
  console.log(`Hello, ${(name || "Guest"} : ${id} `);
}

// ❌
function greet(name?: string, id: number) {
  console.log(`Hello, ${(name || "Guest"} : ${id} `);
}

 

 

✅ rest parameter

... 을 사용하면 rest parameter라고 해서 여러 개의 값을 배열로 받을 수 있어요.

function sum(...numbers: number[]) {
  return numbers.reduce((acc, num) => acc + num, 0);
}

이 함수는 전달된 모든 숫자를 더하는 함수예요. rest 파라미터 덕분에 몇 개의 숫자가 들어오든 처리할 수 있습니다.



02


함수 타입 표현식 (Function Type Expression)

함수 타입 표현식은 여러 개의 함수가 동일한 타입을 가질 때 유용합니다.

 

예를 들어, 덧셈, 뺄셈, 곱셈, 나눗셈 함수를 만들 때 타입을 반복해서 정의해야 할 때,

const add = (a: number, b: number) => a + b;
const sub = (a: number, b: number) => a - b;
const multiply = (a: number, b: number) => a * b;
const divide = (a: number, b: number) => a / b;

 

위의 코드를 함수 타입 표현식을 사용하면 간결하게 만들 수 있어요.

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

const add: Operation = (a, b) => a + b;
const sub: Operation = (a, b) => a - b;
const multiply: Operation = (a, b) => a * b;
const divide: Operation = (a, b) => a / b;

이렇게 타입을 정의하면 같은 타입을 가진 함수들이 많아질 때 유용하죠.



03


호출 시그니쳐 (Call Signature)

호출 시그니쳐는 함수 타입 표현식과 동일하게 함수의 타입을 별도로 정의하는 방식입니다.

위의 코드와 사실 동일한 기능을 하죠. 이렇게 정의할 수도 있어요.

type Operation2 = {
  (a: number, b: number): number;
};

const add2: Operation2 = (a, b) => a + b;
const sub2: Operation2 = (a, b) => a - b;
const multiply2: Operation2 = (a, b) => a * b;
const divide2: Operation2 = (a, b) => a / b;

자바스크립트에서는 함수도 객체이기 때문에, 위 코드처럼 객체를 정의하듯 함수의 타입을 별도로 정의할 수 있습니다.

 

참고로 이때 다음과 같이 호출 시그니쳐 아래에 프로퍼티를 추가 정의하는 것도 가능합니다.

이렇게 할 경우 함수이자 일반 객체를 의미하는 타입으로 정의되며 이를 하이브리드 타입이라고 부릅니다.

type Operation2 = {
  (a: number, b: number): number;
  name: string;
};

const add2: Operation2 = (a, b) => a + b;
(...)

add2(1, 2);
add2.name;


03


함수 타입의 호환성

타입스크립트는 함수 타입이 호환되는지 체크할 수 있습니다.

즉, 하나의 함수가 다른 함수로 대체 가능한지 판단해 주는 거죠. 함수 타입의 호환성은 크게 두 가지로 나눠져요.

1. 반환값 타입 호환성

두 함수의 반환값 타입이 호환되는지 체크해요.

 

예를 들어, 한 함수의 반환값 타입이 number인 경우, 다른 함수의 반환값도 number여야 호환됩니다.

다만, 반환값이 더 좁은 타입에서 더 넓은 타입으로는 호환되지 않아요.

type A = () => number;
type B = () => 10;

let a: A = () => 10;
let b: B = () => 10;

a = b; // ✅
b = a; // ❌

코드를 보면 업캐스팅은 가능하지만 다운캐스팅은 안되는 걸 볼 수 있습니다.

 

2. 매개변수 타입 호환성

두 번째 기준인 매개변수의 타입이 호환되는지 판단할 때에는

두 함수의 매개변수의 개수가 같은지 다른지에 따라 두 가지 유형으로 나뉘게 됩니다.

 

✅ 매개변수의 개수가 같을 때

매개변수의 타입이 더 좁거나 구체적인 함수는 더 넓은 타입을 받는 함수와 호환되지 않아요.

반대로, 매개변수의 타입이 더 넓은 함수는 더 좁은 타입을 받는 함수와 호환됩니다.

type Animal = {
  name: string;
};

type Dog = {
  name: string;
  color: string;
};

let animalFunc = (animal: Animal) => {
  console.log(animal.name);
};

let dogFunc = (dog: Dog) => {
  console.log(dog.name);
  console.log(dog.color);
};

animalFunc = dogFunc; // ❌
dogFunc = animalFunc; // ✅

 

왜 이렇게 되는가 하면 animalFunc = dogFunc를 코드로 표현해 보면 다음과 같습니다. 

let animalFunc = (animal: Animal) => {
  console.log(animal.name);  // ✅
  console.log(animal.color); // ❌
};

animalFunc 타입의 매개변수 타입은 Animal 타입입니다.

그러나 dogFunc 함수 내부에서는 name과 color 프로퍼티에 접근합니다.

따라서 이렇게 할당이 이루어지게 되면 animal.color처럼 존재할 거라고 보장할 수 없는 프로퍼티에 접근하게 됩니다.

 

반대로 dogFunc = animalFunc를 코드로 표현하면 다음과 같습니다.

let dogFunc = (dog: Dog) => {
  console.log(dog.name);
};

dogFunc 함수의 매개변수는 Dog 타입입니다.

그리고 animalFunc 함수 내부에서는 name 프로퍼티에만 접근합니다. 그래서 이 코드는 안전합니다.

 

 

✅ 매개변수의 개수가 다를 때

함수의 매개변수 개수가 다를 때, 매개변수가 적은 함수는 매개변수가 많은 함수와 호환될 수 있습니다.

하지만, 반대로 매개변수가 많은 함수는 매개변수가 적은 함수와 호환되지 않아요.

type Func1 = (a: number, b: number) => void;
type Func2 = (a: number) => void;

let func1: Func1 = (a, b) => {};
let func2: Func2 = (a) => {};

func1 = func2; // ✅
func2 = func1; // ❌

func1 = func2는 허용돼요. 이유는 func2가 a 하나만 받는데, func1은 a, b를 받죠. 

func1이 더 많은 매개변수를 받을 수 있으니까 func2와 호환됩니다.


반대로, func2 = func1은 허용되지 않아요. func1은 a와 b를 받지만, 

func2는 a만 받으니까 func2가 func1을 대체할 수 없어요.



04


함수 오버로딩

함수 오버로딩은 하나의 함수가 여러 가지 버전으로 정의되는 방식이에요.

즉, 매개변수의 개수나 타입에 따라서 함수가 다르게 동작하도록 만들 수 있는 방법입니다.

하지만 자바스크립트에서는 지원되지 않으며, 타입스크립트에서만 지원되는 기능이에요.

1. 함수 오버로딩의 기본 구조

타입스크립트에서는 함수 오버로딩을 구현하려면 먼저 여러 버전의 시그니처(오버로드 시그니처)를 작성하고,

그 후 실제 구현부에서 그 시그니처(구현 시그니처)들을 처리하도록 해야 합니다.

// 오버로드 시그니처 (버전별 함수 타입 정의)
function func(a: number): void;  
function func(a: number, b: number, c: number): void;

// 실제 구현부 (시그니처를 처리하는 부분)
function func(a: number, b?: number, c?: number) {
  if (typeof b === "number" && typeof c === "number") {
    console.log(a + b + c); // 3개의 숫자가 있을 경우
  } else {
    console.log(a * 20); // 1개의 숫자만 있을 경우
  }
}

func(1);        // ✅ 1개 인자 사용 - 첫 번째 버전 호출
func(1, 2);     // ❌ 두 번째 버전 호출 안됨 (타입 에러)
func(1, 2, 3);  // ✅ 3개 인자 사용 - 세 번째 버전 호출

📂 설명

  • 오버로드 시그니처 정의:
    👉 첫 번째는 func(a: number)로, a만 받는 함수의 타입을 정의한 거예요.
    👉 두 번째는 func(a: number, b: number, c: number)로, a, b, c를 받는 함수의 타입을 정의한 거죠.

  • 구현부 작성:
    👉 실제 함수 구현에서는 b와 c가 있을 때와 없을 때로 나누어 처리할 수 있어요.
    👉 b와 c가 모두 숫자일 때는 세 개의 값을 더하고, 하나만 있을 때는 1개의 값만 곱해서 계산하는 방식으로 구현합니다.

2. 왜 함수 오버로딩을 사용할까?

함수 오버로딩을 사용하면 같은 함수 이름으로 여러 버전을 만들 수 있어요.

매개변수의 개수나 타입에 따라 함수가 다르게 동작하게 할 수 있기 때문에, 코드가 더 깔끔하고 재사용하기 쉬워집니다.

 

예를 들어, 숫자 두 개를 더하는 함수가 필요하다면, 숫자 세 개를 더하는 버전을 같은 함수 이름으로 만들 수 있어요.

 

✅ 유의사항

: 오버로드 시그니처는 함수 구현부보다 위에 정의해야 합니다.

실제 구현부에서는 시그니처와 다르게 구현할 수 있지만, 매개변수에 따라 조건을 나누어 처리해야 합니다.



05


사용자 정의 타입가드

사용자 정의 타입가드는 참 또는 거짓을 반환하는 함수를 이용해서 우리가 직접 타입을 확인하는 방법이에요.

이 방법을 사용하면 타입스크립트가 더 정확하게 타입을 구분할 수 있게 도와줍니다.

 

예를 들어, Dog과 Cat 타입이 있을 때, isDog와 isCat 함수를 만들어 각각의 타입을 확인할 수 있어요.

type Dog = {
  name: string;
  isBark: boolean;
};

type Cat = {
  name: string;
  isScratch: boolean;
};

type Animal = Dog | Cat;

// Dog 타입인지 확인하는 타입 가드
function isDog(animal: Animal): animal is Dog {
  return (animal as Dog).isBark !== undefined;
}

// Cat 타입인지 확인하는 타입 가드
function isCat(animal: Animal): animal is Cat {
  return (animal as Cat).isScratch !== undefined;
}

function warning(animal: Animal) {
  // isDog 함수를 사용해서 animal이 Dog인지 확인
  if (isDog(animal)) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } 
  // 그 외는 Cat 타입
  else if (isCat(animal)) {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}

📂 설명

  1. 타입가드 함수는 isDog, isCat처럼 작성됩니다. 이 함수는 해당 객체가 특정 타입인지 확인하고 참/거짓을 반환합니다.
    👉 isDog(animal)은 animal이 Dog 타입일 때만 true를 반환합니다.
    👉 isCat(animal)은 animal이 Cat 타입일 때만 true를 반환합니다.

  2. warning 함수에서 isDog를 사용하면, animal이 Dog 타입인지를 확인하고, 그 타입에 맞는 동작을 합니다. 
    이때 타입이 좁혀져서 animal의 타입을 정확하게 인식할 수 있습니다.

  3. animal is Dog와 같은 타입의 반환은 타입스크립트에게 animal이 Dog 타입이라고 보장해 줍니다.
    그래서 타입이 확정된 후, isDog가 true를 반환하는 조건문 내부에서만 Dog 타입에 접근할 수 있습니다.

" is의 의미 "

 

is는 타입가드 함수에서 중요한 역할을 합니다. TypeScript에서 is는 타입을 좁히는 역할을 합니다.

 

즉, is는 TypeScript에게 "이 타입이 맞다는 걸 보장할 때" 사용되고,

그 결과로 해당 변수의 타입을 명확히 좁혀서 더 안전하게 사용할 수 있게 도와줍니다.



06


마무리

오늘은 TypeScript에서 함수 타입, 함수 오버로딩, 사용자 정의 타입가드에 대해 알아봤습니다.

이 기능들을 잘 활용하면, 우리가 다루는 데이터의 타입을 더 정확하게 정의하고 관리할 수 있어요.

  • 함수 타입 표현식을 정의함으로써 함수의 타입을 명확하게 설정하고 재사용성을 높일 수 있습니다.
  • 호출시그니처는 함수 타입을 객체 형식으로 정의하여, 다른 속성을 추가하고 타입을 명확히 설정할 수 있게 도와준다.
  • 함수 타입 정의를 통해 매개변수랑 반환값의 타입을 확실히 설정할 수 있습니다.
  • 함수 오버로딩으로 하나의 함수로 여러 가지 버전의 처리를 할 수 있죠.
  • 사용자 정의 타입가드는 객체가 어떤 타입인지 정확하게 체크해 줘서 코드가 더 안전해집니다.

이렇게 타입을 명확하게 지정하면, 코드에서 실수를 줄일 수 있고 유지보수도 쉬워집니다!!