[4] 함수와 타입

Doozuu·2023년 10월 2일
0

TypeScript

목록 보기
8/13

📌 함수 타입

함수의 타입을 정의하는 방법

다음과 같은 자바스크립트 함수가 있을 때, 이 함수를 다른 사람에게 설명하는 가장 좋은 방법은 이 함수가 어떤 매개변수를 받고 어떤 값을 반환 하는지 이야기 하는 것이다.

// 함수를 설명하는 가장 좋은 방법
// 어떤 매개변수를 받고, 어떤 결과값을 반환하는지 설명
function func(a, b) {
  return a + b;
}

타입스크립트에서는 이와 유사하게 어떤 타입의 매개변수를 받고, 어떤 타입의 값을 반환하는지 이야기 하면 된다.

따라서 함수의 타입은 다음과 같이 매개 변수와 반환값의 타입으로 결정된다.
(참고로 함수의 반환값 타입은 자동으로 추론되기 때문에 생략해도 된다.)

function func(a: number, b: number): number {
  return a + b;
}

화살표 함수 타입 정의하기

화살표 함수의 타입 정의 방식은 다음과 같다.

const add = (a: number, b: number): number => a + b;

매개변수 기본값 설정하기

다음과 같이 함수의 매개변수에 기본값이 설정되어있으면 타입이 자동으로 추론된다.
이럴 경우 타입 정의를 생략해도 된다.

function introduce(name = "이정환") {
	console.log(`name : ${name}`);
}

선택적 매개변수 설정하기

다음과 같이 매개변수의 이름뒤에 물음표(?)를 붙여주면 선택적 매개변수가 되어 생략이 가능하다.

function introduce(name = "이정환", tall?: number) {
  console.log(`name : ${name}`);
  console.log(`tall : ${tall}`);
}

introduce("이정환", 156);

introduce("이정환");

위 코드의 tall 같은 선택적 매개변수의 타입은 자동으로 undefined와 유니온 된 타입으로 추론된다.

따라서 tall의 타입은 현재 number | undefined이 된다.

그러므로 이 값이 number 타입의 값일 거라고 기대하고 사용하려면 다음과 같이 타입 좁히기가 필요하다.

function introduce(name = "이정환", tall?: number) {
  console.log(`name : ${name}`);
  if (typeof tall === "number") {
    console.log(`tall : ${tall + 10}`);
  }
}

또 한가지 주의할 점은 선택적 매개변수는 필수 매개변수 앞에 올 수 없다. 반드시 뒤에 배치해야 한다.

function introduce(name = "이정환", tall?: number, age: number) {
	// 오류!
  console.log(`name : ${name}`);
  if (typeof tall === "number") {
    console.log(`tall : ${tall + 10}`);
  }
}

나머지 매개변수

다음과 같이 여러개의 숫자를 인수로 받는 함수가 있다고 가정하자.

function getSum(...rest) {
  let sum = 0;
  rest.forEach((it) => (sum += it));
  return sum;
}

getSum 함수는 나머지 매개변수 rest로 배열 형태로 number 타입의 인수들을 담은 배열을 전달받는다.
이때 rest 파라미터의 타입은 다음과 같이 정의하면 된다.

function getSum(...rest: number[]) {
  let sum = 0;
  rest.forEach((it) => (sum += it));
  return sum;
}



📌 함수 타입 표현식과 호출 시그니쳐

함수 타입 표현식

다음과 같이 함수 타입을 타입 별칭과 함께 별도로 정의할 수 있다. 이를 함수 타입 표현식(Function Type Expression)이라고 부른다.


이렇게 함수 타입 표현식을 이용하면 함수 선언 및 구현 코드와 타입 선언을 분리할 수 있어 유용하다.

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

const add: Add = (a, b) => a + b;

함수 타입 표현식은 다음과 같이 여러개의 함수가 동일한 타입을 갖는 경우에 요긴하게 사용된다.

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;

호출 시그니쳐

호출 시그니쳐(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);
add.name;



📌 함수 타입의 호환성

함수 타입의 호환성이란 특정 함수 타입을 다른 함수 타입으로 괜찮은지 판단하는 것을 의미한다.

다음 2가지를 기준으로 함수 타입의 호환성을 판단하게 된다.

  • 두 함수의 반환값 타입이 호환되는가?
  • 두 함수의 매개변수의 타입이 호환되는가?

기준 1 : 반환값 타입이 호환되는가?

A와 B 함수 타입이 있다고 가정할 때 A 반환값 타입이 B 반환값 타입의 슈퍼 타입이라면 두 타입은 호환된다.

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

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

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

기준 2 : 매개변수의 타입이 호환되는가?

두번째 기준인 매개변수의 타입이 호환되는지 판단할 때에는 두 함수의 매개변수의 개수가 같은지 다른지에 따라 두가지 유형으로 나뉘게 된다.

2-1. 매개변수의 개수가 같을 때

두 함수 타입 C와 D가 있다고 가정할 때 두 타입의 매개변수의 개수가 같다면 C 매개변수의 타입이 D 매개변수 타입의 서브 타입일 때에 호환된다.

type C = (value: number) => void;
type D = (value: 10) => void;

let c: C = (value) => {};
let d: D = (value) => {};

c = d; // ❌
d = c; // ✅

C 매개변수의 타입은 Number, D 매개변수의 타입은 Number Literal 이다.
따라서 C 매개변수의 타입이 D 매개변수의 슈퍼타입이므로 D를 C로 취급하는것은 불가능하나 반대로는 가능하다.

이는 반환값 타입과 반대된다.

이렇게 되는 이유는 두 함수의 매개변수의 타입이 모두 객체 타입일때 좀 더 두드러진다.

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를 할당하는 것은 불가능하다. dogFunc의 매개변수 타입이 animalFunc 매개변수 타입보다 작은 서브타입이기 때문이다. 반대로는 가능하다.

animalFunc = dogFunc를 코드로 표현해보면 다음과 같다.

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

2-2. 매개변수의 개수가 다를 때

매개변수의 개수가 다를 때에는 비교적 간단하다.

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; // ❌



📌 함수 오버로딩

함수 오버로딩이란 하나의 함수를 매개변수의 개수나 타입에 따라 다르게 동작하도록 만드는 문법이다.

타입스크립트에서 함수 오버로딩을 구현하려면 먼저 다음과 같이 버전별 오버로드 시그니쳐를 만들어 줘야 한다.

// 버전들 -> 오버로드 시그니쳐
function func(a: number): void;
function func(a: number, b: number, c: number): void;

이렇게 구현부 없이 선언부만 만들어둔 함수를 ‘오버로드 시그니쳐’라고 한다.

위 코드에서는 2개의 오버로드 시그니쳐를 만들었으며 각각 함수의 버전을 의미한다.
위 코드는 func 함수는 매개변수를 1개 받는 버전과 3개 받는 2개의 버전이 있다고 알리는 것과 같다.


오버로드 시그니쳐를 만들었다면 다음으로는 구현 시그니쳐를 만들어줘야 한다.

구현 시그니쳐는 실제로 함수가 어떻게 실행될 것인지를 정의하는 부분이다.

// 버전들 -> 오버로드 시그니쳐
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);
  } else {
    console.log(a * 20);
  }
}

func(1);        // ✅ 버전 1 - 오버로드 시그니쳐
func(1, 2);     // ❌ 
func(1, 2, 3);  // ✅ 버전 3 - 오버로드 시그니쳐

구현 시그니쳐의 매개변수 타입은 모든 오버로드 시그니쳐와 호환되도록 만들어야 한다.

따라서 위 코드에서는 매개변수 b와 c를 선택적 매개변수로 만들어 매개변수를 하나만 받는 첫번째 오버로드 시그니쳐와도 호환되도록 만들어 주었다.



📌 사용자 정의 타입가드

사용자 정의 타입가드란 참 또는 거짓을 반환하는 함수를 이용해 우리 입맛대로 타입 가드를 만들 수 있도록 도와주는 타입스크립트의 문법이다.

아래처럼 in 연산자를 이용해 타입을 좁히는 방식은 좋지 않다.

예를 들어 만약 Dog 타입의 프로퍼티가 다음과 같이 중간에 이름이 수정되거나 추가 또는 삭제될 경우에는 타입 가드가 제대로 동작하지 않을 수도 있다.

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

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

type Animal = Dog | Cat;

function warning(animal: Animal) {
  if ("isBark" in animal) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } else if ("isScratch" in animal) {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}

따라서 이럴 때에는 다음과 같이 함수를 이용해 커스텀 타입 가드를 만들어 타입을 좁히는게 더 좋다.

(...)

// 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) {
  if (isDog(animal)) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } else {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}

isDog 함수는 매개변수로 받은 값이 Dog 타입이라면 true 아니라면 false를 반환한다.

이때 반환값의 타입으로 animal is Dog 를 정의하면 이 함수가 true를 반환하면 조건문 내부에서는 이 값이 Dog 타입임을 보장한다는 의미가 된다.

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글