본문 바로가기
Typescript

Typescript | 함수 타입, 함수 타입 표현식과 호출 시그니처, 호환성

by 리잼 2024. 11. 2.
반응형

함수 타입

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

  • 다음과 같은 자바스크립트 함수가 있을 때, 이 함수를 다른 사람에게 설명하는 가장 좋은 방법은
    이 함수가 어떤 매개변수를 받고 어떤 값을 반환 하는지 이야기 하는 것 이다.
// 함수를 설명하는 가장 좋은 방법
// 어떤 매개변수를 받고, 어떤 결과값을 반환하는지 설명
function func(a, b) {
  return a + b;
}
  • TS 에서 함수 정의하는 방법
// 매개변수의 타입과 반환 값의 타입을 넣어주면 된다.
// 함수의 반환 값은 자동으로 추론되어 생략이 가능하다.
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:number = "이재민") {
	console.log(`name : ${name}`);
}

// 당연히 다른 타입의 값을 인수로 전달해도 오류가 발생한다.
function introduce(name = "이재민") {
  console.log(`name : ${name}`);
}

introduce(1); // 오류

선택적 매개변수 설정하기

  • 매개변수의 이름뒤에 ? 를 붙여주면 선택적 매개변수가 되어 생략이 가능하다.
function introduce(name = "이재민", tall?: number) {
  console.log(`name : ${name}`);
  console.log(`tall : ${tall}`);
}

introduce("이재민", 174);

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 getSum(...rest: [number, number, number]) {
  let sum = 0;
  rest.forEach((it) => (sum += it));
  return sum;
}

getSum(1, 2, 3)    // ✅
getSum(1, 2, 3, 4) // ❌

함수 타입 표현식

함수 타입을 타입 별칭과 함께 별도로 정의할 수 있다. 이를 함수 표현식이라고 부른다.

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

const add: Add = (a, b) => a + b;
  • 변수 add 의 타입을 함수 타입 표현식으로 정의한 함수 타입으로 정의
  • 함수 타입 표현식을 이용하면 함수 선언 및 구현 코드와 타입 선언을 분리할 수 있어 유용하다.
  • 함수 타입 표현식은 다음과 같이 여러개의 함수가 동일한 타입을 같는 경우에 요긴하게 사용된다.
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;

 

호출 시그니쳐

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

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;
  • JS 에서는 함수도 객체이기 때문에, 위 코드 처럼 객체를 정의하듯 함수의 타입을 별도로 정의할 수 있다.
  • 참고로 이때 다음과 같이 호출 시그니처 아래에 프로퍼티를 추가 정의하는 것도 가능하다.
  • 이럴 경우 함수이자 일반 객체를 의미하는 타입으로 정의되며 이를 하이브리드 타입이라고 부른다.
type Operation2 = {
  (a: number, b: number): number;
  name: string;
};

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

add2(1, 2);
add2.name;

함수 타입의 호환성

특정 함수 타입을 다른 함수 타입으로 괜찮은지 판단하는 것을 의미

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

  1. 두 함수의 반환값 타입이 호환되는가?
  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); // ❌
};
  • animalFunc 타입의 매개변수 타입은 Animal 타입이다.
  • dogFunc 함수 내부에서는 name과 color 프로퍼티에 접근.
  • 따라서 이렇게 할당이 이루어지게 되면 animal.color처럼 존재할거라고 보장할 수 없는 프로퍼티에 접근하게 된다.
  • 반대로 dogFunc = animalFunc를 코드로 표현하면 다음과 같다.
let dogFunc = (dog: Dog) => {
  console.log(dog.name);
};
  • dogFunc 함수의 매개변수는 Dog 타입이다.
  • animalFunc 함수 내부에서는 name 프로퍼티에만 접근한다.
  • 그러므로 두개의 함수 타입 C와 D가 있을 때 두 매개변수의 개수가 같을 경우 D를 C로 취급하려면
    C 매개변수의 타입이 D 매개변수 타입의 서브 타입이어야 한다.

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

 

반응형