💡 함수 타입을 정의하는 방법
• 일반 함수 타입 정의하기
// 자바스크립트
function func(a, b) {
return a + b;
}
다음과 같은 자바스크립트 함수가 있을 때 다른 사람에게 함수를 설명하는 가장 좋은 방법은 이 함수가 어떤 매개변수를 받고, 어떤 값을 반환하는지 이야기하는 것입니다.
// 타입스크립트
function func(a: number, b: number): number {
return a + b;
}
function func(a: number, b: number) {
return a + b;
}
이번 코드는 타입스크립트 코드로, 자바스크립트의 방식과 비슷합니다. 딱 타입만 추가된 것이죠. 타입스크립트에서는 어떤 타입의 매개변수를 받고, 어떤 타입의 결과값을 반환하는지를 설명하면 됩니다. 그리고 함수의 반환값 타입은 매개변수를 통하여 자동으로 추론되기 때문에 두번째에 작성한 함수코드처럼 반환값 타입은 생략 가능합니다.
• 화살표 함수 타입 정의하기
const add = (a: number, b: number): number => a + b;
const add = (a: number, b: 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); // ❌
기본값이 "수현"이라는 string 타입인데 number라는 다른 타입으로 매개변수의 타입을 정의하면 오류가 발생합니다. 또한 기본값이 "수현"으로 정의되어 있는데 다른 타입의 값을 인수로 전달하여도 오류가 발생하게 됩니다.
• 선택적 매개변수 설정하기
function introduce(name: "수현", age: number, tall?: number) {
console.log(`name: ${name}`);
if (typeof tall === "number") {
console.log(`tall: ${tall + 10}`);
}
}
introduce("수현", 27, 173);
introduce("수현", 27);
다음과 같이 매개변수의 이름 바로 뒤에 물음표(?)를 붙여주면 선택적 매개변수가 되어 생략이 가능합니다. 위 코드의 tall 같은 선택적 매개변수의 타입은 자동으로 undefined와 유니온된 타입으로 추론됩니다. 따라서 tall의 현재 타입은 number | undefined 가 됩니다. 따라서 이 값이 number 타입의 값일 것이라고 기대하고 사용하기 위해서는 typeof 와 함께 타입 좁히기 과정이 필요합니다.
그리고 선택적 매개변수를 배치하는 데에는 중요한 규칙이 있습니다. 선택적 매개변수는 필수 매개변수 앞에 위치할 수 없으며, 반드시 뒤에 배치되어야합니다. 선택적 매개변수(tall) 뒤에 필수 매개변수(age)가 위치해 있기 때문에 필수 매개변수 아래에 빨간 줄 오류가 뜨게된 것입니다.
• 나머지 매개변수
function getSum(...rest: number[]) {
let sum = 0;
rest.forEach((it) => (sum += it));
return sum;
}
다음과 같이 rest 파라미터를 통해서 여러개의 숫자를 인수로 받는 함수가 있다고 가정하겠습니다. getSum 함수는 나머지 매개변수 rest로 배열 형태로 number 타입의 인수들을 담은 배열을 전달받습니다. 이때 rest 파라미터의 타입을 number[ ]과 같이 정의하면 됩니다.
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, 5); // ❌
만약 나머지 매개변수의 길이를 고정하고 싶다면 다음과 같이 튜플 타입을 사용해도 됩니다. 저는 배열의 길이를 3으로 고정하고 싶어서 [number, number, number] 으로 작성하였습니다.
💡 함수 타입 표현식
type Add = (a: number, b: number) => number;
const add: Add = (a, b) => a + b;
다음과 같이 함수 타입을 타입 별칭과 함께 별도로 정의할 수 있으며 이를 함수 타입 표현식 (Function Type Expression)이라고 부릅니다. 변수 add의 타입을 함수 타입 표현식으로 정의한 함수 타입으로 정의하였습니다.
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;
함수 타입 표현식은 다음과 같이 여러개의 함수가 동일한 타입을 갖는 경우에 유용하게 사용되며, 나중에 동일한 타입의 함수가 추가되어도 타입 주석을 이용해 타입 정의만 해주면 되기 때문에 매우 편리합니다.
const addEx: (a: number, b: number) => number = (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; // 추가프로퍼티
};
add2(1, 2);
add2.name;
참고로 다음과 같이 호출 시그니쳐 아래에 프로퍼티를 추가적으로 정의하는 것 또한 가능합니다. 이러한 경우 함수이자 일반 객체를 의미하는 타입으로 정의되며 이를 하이브리드 타입이라고 부릅니다.
💡 함수 타입의 호환성
함수 타입의 호환성이란 특정 함수 타입을 다른 함수 타입으로 취급해도 괜찮은지를 판단하는 것을 의미합니다. 함수 타입의 호환성을 판단하는 다음과 같습니다.
⓵ 두 함수의 반환값 타입이 호환되는가?
⓶ 두 함수의 매개변수의 타입이 호환되는가?
• 기준 1. 반환값 타입이 호환되는가?
type A = () => number;
type B = () => 10;
let a: A = () => 10;
let b: B = () => 10;
a = b;
b = a; // ❌
다음과 같이 함수 타입 A와 B가 있다고 가정할 때 A 반환값 타입이 B 반환값 타입의 슈퍼타입이라면 두 타입은 호환된다. 현재 A의 반환값 타입은 Number, B의 반환값 타입은 Number Literal 입니다. 따라서 변수 a에 b를 할당하는 것은 가능하고 그 반대로는 불가능합니다.
• 기준2. 매개변수의 타입이 호환되는가?
⓵ 매개변수의 개수가 같을 때
type C = (vaule: number) => void;
type D = (vaule: 10) => void;
let c: C = (value) => {};
let d: D = (value) => {};
c = d; // ❌
d = c;
다음과 같이 함수 타입 C와 D가 있다고 가정할 때 두 타입의 매개변수의 개수가 같다면 C 매개변수의 타입이 D 매개변수의 서브타입일 때 호환 가능합니다. 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 매개변수의 타입보다 작은 서브타입이기 때문입니다.
헷갈리는 개념이기는 한데요 이렇게 생각하면 쉽습니다. 각각의 함수에 타입의 프로퍼티를 모두 콘솔로 최대한 출력하도록 위와 같이 코드를 작성하였습니다. 1줄짜리의 콘솔이 있는 animalFunc에 2줄짜리의 콘솔이 있는 dogFunc가 들어가는 것은 불가능할 것 같죠? 반대로 2줄짜리의 콘솔이 있는 dogFunc에 1줄짜리의 콘솔이 있는 animalFunc가 들어가는 것은 가능할 것 같지 않나요? 적은 양의 콘솔이 있는 함수가 많은 양의 콘솔이 있는 함수에 들어가는 것이 더 가능해보이니까 이해하기 쉽죠?
⓶ 매개변수의 개수가 다를 때
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; // ❌
매개변수의 개수가 다를 떄에는 비교적 간단합니다. 매개변수를 적게 받는 타입의 함수가 매개변수를 더 많이 받는 타입의 함수에 저장될 수 있습니다.
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 인터페이스의 활용: 기본 개념부터 확장과 선언 합치기 (0) | 2024.12.21 |
---|---|
[ TypeScript ] 함수 오버로딩과 커스텀 타입가드로 복잡한 타입 관리하기 (0) | 2024.12.19 |
[ TypeScript ] 타입 단언과 타입 좁히기 활용법, 서로소 유니온 타입의 이해 (0) | 2024.12.19 |
[ TypeScript ] 대수 타입 & 타입 추론: 강력한 타입 시스템 이해하기 (0) | 2024.12.19 |
[ TypeScript ] 타입 계층도와 함께 살펴보는 타입 호환성 (1) | 2024.12.19 |