📘

5장 함수

5.1 함수의 선언과 호출 방법

5.1.1 함수의 선언

타입스크립트에서 함수를 선언할 때 매개변수의 타입, 반환 값의 타입에 대한 명시를 할 수 있으며, 이를 통해 명확한 코드를 작성이 가능합니다. 함수를 정의하는 방법으로는 함수 선언식, 함수 표현식, 화살표 함수 등이 있으며 함수를 호출하는 방법은 자바스크립트와 동일합니다. 아래의 코드를 보며 타입이 명시되었을 때의 구조를 눈으로 익히고, 매개변수 및 반환 값 타입에 대한 설명은 목차 5.1.2부터 이어가도록 하겠습니다.
// 함수 선언식 function addNumber(a: number, b: number): number { return a + b; } console.log(addNumber(5, 15)); // 20 // 함수 표현식 let multiplyNumber = function (a: number, b: number): number { return a * b; }; console.log(multiplyNumber(4, 5)); // 20 // 화살표 함수 let divideNumber = (a: number, b: number): number => { return a / b; }; console.log(divideNumber(24, 3)); // 8
💡
타입스크립트에서 함수의 반환 값 타입은 자동으로 추론되기 때문에 생략이 가능합니다!
// 함수 선언식 반환 값 타입 생략 function addNumber(a: number, b: number) { return a + b; } console.log(addNumber(5, 15)); // 20 // 함수 표현식 반환 값 타입 생략 let multiplyNumber = function (a: number, b: number) { return a * b; }; console.log(multiplyNumber(4, 5)); // 20 // 화살표 함수 반환 값 타입 생략 let divideNumber = (a: number, b: number) => { return a / b; }; console.log(divideNumber(24, 3)); // 8

5.1.2 함수의 매개변수

1) 매개변수 선언 방법

함수의 매개변수는 괄호 안에 선언하며, 각 매개변수는 콤마로 구분됩니다. 타입스크립트에서는 매개변수 이름 뒤에 콜론(:)과 타입을 명시하여 매개변수의 타입을 선언합니다.
function add(a: number, b: number): number { return a + b; } console.log(add(1, 2)); // 3

2) 기본 매개변수

함수 호출 시 전달되는 값이 없으면 기본값을 설정할 수 있습니다. 매개변수 선언 후 등호(=)를 사용하여 기본값을 명시합니다.
// 매개면수 greeting의 기본값을 'Hello'로 설정했습니다. function greet(name: string, greeting: string = "Hello"): void { console.log(`${greeting}, ${name}!`); } greet("John", "Hi"); // Hi, John! // 매개변수 greeting에 해당하는 값을 인자로 넘기지 않아 기본값인 'Hello'를 출력했습니다. greet("John"); // Hello, John!

3) 선택적 매개변수

선택적 매개변수는 필요에 따라 전달할 수도, 전달하지 않을 수도 있는 매개변수입니다. 매개변수 이름 뒤에 물음표(?)를 붙여 선언합니다.
function greetSomeone(name: string, greeting?: string): void { if (greeting) { console.log(`${greeting}, ${name}!`); } else { console.log(`Hello, ${name}!`); } } greetSomeone('John'); // Hello, John! greetSomeone('John', 'Hi'); // Hi, John!

4) 나머지 매개변수

나머지 매개변수는 개수가 정해지지 않은 여러 개의 매개변수를 배열로 받을 수 있습니다. 선언 시 매개변수 이름 앞에 점 세 개(...)를 붙입니다.
function sum(...numbers: number[]): number { return numbers.reduce((prev, curr) => prev + curr, 0); } console.log(sum(1, 2, 3)); // 6

5.1.3 함수의 반환 값

함수의 반환 타입은 함수가 어떤 값을 반환하는지, 또는 아무런 값을 반환하지 않는지를 명확하게 나타내줍니다.

1) 기본 반환 타입

타입스크립트에서 함수의 반환 타입은 함수 선언의 괄호 다음에 콜론(:)과 타입을 명시하여 선언합니다. 만약 반환 타입을 명시하지 않으면, 타입스크립트 컴파일러는 함수의 본문을 분석하여 반환 타입을 유추합니다.
// ↓ : number로 반환 타입 명시 function addNember(a: number, b: number): number { return a + b; } let result = addNember(11, 22); // 반환 값 33의 타입은 number입니다.
위의 예제에서 add 함수는 두 개의 숫자를 더한 결과를 반환하므로, 반환 타입은 number입니다.

2) void 반환 타입

void는 특별한 타입으로, 어떤 값도 가지지 않는 상태를 나타냅니다. 함수가 반환 값을 가지지 않는 경우 반환 타입으로 void를 사용합니다.
function log(message: string): void { console.log(message); } log("Hello, TypeScript!"); // 출력: Hello, TypeScript!
위의 예제에서 log 함수는 출력을 수행하지만 아무런 값을 반환하지 않습니다. 이러한 경우 반환 타입으로 void를 사용합니다.

3) never 반환 타입

never는 절대 발생하지 않을 값의 타입을 나타냅니다. 함수가 오류를 발생시키거나, 무한히 계속 실행되어 절대적으로 반환 값이 없을 때 never 타입을 사용합니다.
function error(message: string): never { throw new Error(message); } error('Something went wrong'); // Error: Something went wrong
위의 예제에서 error 함수는 항상 Error를 발생시키므로, 이 함수의 반환 타입은 never입니다. 이러한 never 타입은 프로그램의 특정 부분이 절대 발생하지 않아야 함을 컴파일러에 알려주는 역할을 합니다.
 

5.2 함수의 this 타입

타입스크립트에서 함수 내의 this 타입을 다루는 것은 객체 지향 프로그래밍의 중요한 부분 중 하나입니다. 함수 내에서 this를 사용할 때에는 2가지 방법이 있는데 첫 번째로 this의 타입을 명시적으로 선언하는 방법과 두 번째로 this의 타입을 암시적으로 결정하는 방법이 있습니다. 이 두 방법의 차이점에 대해 이해하고 상황에 맞게 올바르게 사용할 수 있다면 코드의 가독성과 유지 보수를 크게 향상시킬 수 있습니다.

5.2.1 명시적 this 타입

타입스크립트에서는 함수의 첫 번째 매개변수에 this 타입을 명시적으로 선언할 수 있습니다. 이 방법은 주로 객체의 메서드 내에서 this 타입을 통해 구체적으로 접근할 때 유용합니다. 이때 함수의 첫 번째 매개변수로 선언된 this 타입은 함수 호출 시 제공되는 실제 인자가 아니라, 함수가 호출될 객체의 타입을 명시하기 위해 사용됩니다.
interface Clock { time: Date; setTime(date: Date): void; } function setClockTime(this: Clock, date: Date): void { this.time = date; } const clock: Clock = { time: new Date(), setTime: setClockTime, } clock.setTime(new Date()); console.log(clock.time);
setClockTime 함수는 첫 번째 매개변수로 this 타입을 Clock으로 명시적으로 선언하는데 이는 setClockTime 함수가 인터페이스로 구현된 Clock 객체 내에서 호출되는 것을 의미하고 있습니다. 이런 방식으로 this 타입을 명시적으로 선언함으로써, 함수 내에서 this를 사용하면 타입스크립트가 해당 객체의 메서드에 대한 타입 검사를 수행할 수 있게 됩니다.
위 예제에서는 clock의 메서드에서 setClockTime 함수가 할당된 setTime을 호출하고 있습니다. 이 과정에서 this 타입의 매개변수에 대한 인자는 넘기지 않고 new Date()만을 넘겨주고 있습니다. 이는 해당 this 타입 매개변수에 대한 인자는 clock 객체가 넘어가고 있는 것입니다. 그리고 해당 함수 내부 구현부에서 this.timeclock 객체 내의 time을 가리키고 있으며 해당 메서드에 인자로 넘어간 date를 할당받게 됩니다.
이를 통해 명시적 this 타입 선언은 타입스크립트의 타입 시스템을 활용하여 더 안전하고 예측 가능한 코드를 작성할 수 있게 해주며 개발자는 this가 가리키는 객체가 올바른 타입을 가지고 있는지 실제 프로그램을 실행하기 전에 확인할 수 있기 때문에 오류를 줄일 수 있습니다.

5.2.2 암시적 this 타입

암시적 this 타입은 함수가 특정 객체의 메서드로 사용될 때 타입스크립트가 자동으로 this의 타입을 추론합니다. 이 방법은 별도로 타입을 선언하지 않고 this를 사용할 수 있게 해주며 코드를 더 간결하게 작성할 수 있다는 장점이 있습니다. 암시적 this 타입은 함수가 객체 내에 존재할 때, 해당 함수의 this가 자동으로 그 객체를 참조하도록 타입스크립트가 가정하는 것을 말합니다. 이 경우, 우리가 별도로 this가 해당 객체를 지칭하도록 명시해 주지 않아도 됩니다.
interface Clock { time: Date; setTime(date: Date): void; getTime(): Date; } const clock: Clock = { time: new Date(), setTime(date: Date) { this.time = date; }, getTime() { return this.time; }, }; clock.setTime(new Date()); console.log(clock.getTime());
위의 예제처럼 clock 객체는 함수 setTimegetTime 두 가지의 메서드를 포함하고 있습니다. 여기서 setTime의 구현부에 사용된 this는 암시적으로 clock 객체를 가리키고 있으며 getTime의 구현부 또한 this가 clock 객체를 가리키고 있습니다. 이는 타입스크립트가 자동으로 함수의 this가 해당 객체를 가리킨다고 추론하고 있습니다. 이렇게 암시적 this 타입 추론은 타입스크립트의 객체 지향 프로그래밍을 더욱 강력하고 유연하게 만들어줍니다. 이 방식은 객체의 메서드 내에서 this를 사용하여 해당 객체의 다른 프로퍼티나 메서드에 접근할 수 있도록 도와주며 가독성을 향상시켜줍니다.
암시적 this 타입은 타입스크립트가 제공해 주는 타입 시스템의 자연스러운 부분으로 객체의 메서드 내에서 this의 사용을 더욱 직관적이고 안전하게 만들어줍니다. 이를 통해 프로그램을 실행하기 전에 오류를 줄이면서 효율적으로 프로그램을 개발할 수 있습니다.
💡
명시적 this 타입은 함수가 특정 객체 속에서 실행된다는 것을 명확하게 명시하고 싶을 때 사용하는 것이 유용합니다. 이를 통해 타입 안정성을 높이고 코드를 명확하게 표현해 줄 수 있습니다. 암시적 this 타입은 타입스크립트의 타입 추론 기능을 활용하여 코드를 간결하게 줄일 수 있고 효율적으로 개발을 할 수 있도록 도와주게 됩니다. 상황에 따라 적절하게 this 타입을 사용하게 되면 개발의 효율성과 안정성을 크게 향상시킬 수 있을 것입니다.

5.3 오버로딩

오버로딩이란 동일한 이름을 가진 함수를 매개 변수의 개수나 타입에 따라서 여러 버전으로 정의하는 방법입니다. 타입스크립트에서 함수 오버로딩을 사용하는 주된 이유는 함수의 유연성을 증가시키고, 타입 안정성을 보장하며, 코드의 명확성을 강화하기 위함입니다. 함수 오버로딩을 사용하면 하나의 함수 이름으로 다양한 동작을 수행할 수 있으며, 매개변수의 타입이나 개수에 따라 함수의 동작을 다르게 설정할 수 있습니다. 또한, 오버로딩을 통해 컴파일 타임에 타입 오류를 사전에 찾아낼 수 있어 타입의 안정성을 보장합니다. 마지막으로, 함수의 동작을 시그니처를 통해 명확하게 표현함으로써 함수 사용자가 기대하는 입력과 출력을 이해하기 쉽게 만들어줍니다.

5.3.1 오버로드 시그니처

타입스크립트에서 함수 오버로딩을 구현하려면, 해당 함수의 여러 버전을 먼저 선언해야 합니다. 이때 사용하는 선언들을 '오버로드 시그니처'라고 합니다. 오버로드 시그니처는 함수의 본문(구현부) 없이 매개변수 타입과 반환 타입만을 명시합니다. 덧셈 함수를 오버로딩하는 경우를 살펴보겠습니다.
//오버로드 시그니처 function add(a: number, b: number): number; function add(a: string, b: string): string; // Error 함수 구현이 없거나 선언 바로 다음에 나오지 않습니다.
여기서 add 함수는 두 가지 버전이 있음을 알 수 있습니다. 하나는 두 개의 숫자를 더하는 버전이고, 다른 하나는 두 개의 문자열을 연결하는 버전입니다. 첫 번째 오버로드 시그니처 add(a: number, b: number): number;는 두 개의 숫자를 매개변수로 받아 결과를 숫자로 반환합니다. 두 번째 오버로드 시그니처 add(a: string, b: string): string;는 두 개의 문자열을 매개변수로 받아 결과를 문자열로 반환합니다. 오버로드 시그니처만 작성하고, 해당 시그니처를 구현하는 함수를 작성하지 않으면 타입스크립트는 오류를 발생시킵니다. 이는 타입스크립트 컴파일러가 오버로드 시그니처에 따른 호출 가능 함수를 찾지 못하기 때문입니다.

5.3.2 구현 시그니처

타입스크립트에서 함수 오버로딩을 구현하기 위해서는 먼저 오버로드 시그니처를 선언하고, 이후에 실제 함수의 구현부를 작성해야 합니다. 이 구현부를 '구현 시그니처' 또는 '구현 함수'라고 부릅니다. 구현 시그니처는 오버로딩된 모든 시그니처를 포괄하는 로직을 제공해야 합니다. 따라서 구현 시그니처의 매개변수 타입과 반환 타입은 오버로딩된 모든 시그니처를 포괄할 수 있어야 합니다.
// 구현 시그니처 function add(a: number | string, b: number | string): number | string { if (typeof a === "number" && typeof b === "number") { return a + b; } else if (typeof a === "string" && typeof b === "string") { return a.concat(b); } else { throw new Error("Invalid types for addition"); } } console.log(add(1, 2)); // 3 console.log(add("Hello", "World")); // "HelloWorld" console.log(add(1, 2, 3)); //Error 2개의 인수가 필요한데 3개를 가져왔습니다. console.log(add(true, false)); //Error 이 호출과 일치하는 오버로드가 없습니다.
함수 add는 두 개의 인자 ab를 받아들이는데, 이 두 인자는 모두 number 타입 또는 string 타입 일 수 있습니다. 함수를 호출할 때 필요한 인자의 수가 맞지 않거나, 인자의 타입이 맞지 않으면 오류가 발생합니다. 이를 통해 타입스크립트는 함수 호출의 안정성을 보장합니다.
함수 내부에서는 타입 가드(Type Guard)인 typeof 연산자를 사용하여 인자들의 실제 타입을 체크하고 있습니다. 타입 가드를 통해 ab가 모두 number 타입일 때는 두 숫자를 더하고, 모두 string 타입일 때는 두 문자열을 연결합니다.add 함수는 두 개의 인자를 요구하는데, 첫 번째 오류는 세 개의 인자를 전달하려고 했기 때문에 발생했습니다. 두 번째 오류는 add함수가 number 또는 string 타입의 인자만 받을 수 있는데, boolean 타입의 인자가 입력되었기 때문에 발생했습니다.
만약 오버로드 함수의 시그니처들이 서로 다른 개수의 매개변수를 가질 때, 선택적 매개변수를 사용하여 모든 오버로드 시그니처들이 적절한 의미가 있도록 해야 합니다. 이렇게 하면 함수의 유연성을 높이면서도 타입 안전성을 보장할 수 있습니다.
//오버로드 시그니처 function greet(name: string): string; function greet(name: string, age: number): string; // 구현 시그니처 function greet(name: string, age?: number): string { if (age !== undefined) { return `안녕하세요, 제 이름은 ${name}이고, 나이는 ${age}살입니다.`; } else { return `안녕하세요, 제 이름은 ${name}입니다.`; } } console.log(greet('김지윤')); // "안녕하세요, 제 이름은 김지윤입니다." console.log(greet('김지윤', 3)); // "안녕하세요, 제 이름은 김지윤이고, 나이는 3살입니다."
이렇게 오버로드 시그니처를 가진 함수를 호출할 때, 인수들의 타입은 오버로드 시그니처들 중 어느 하나를 따르게 됩니다. 이는 함수의 동작을 더 명확하게 이해할 수 있게 도와줍니다.

5.3.3 예시

1) 타입별칭 예시

type FoodOrder = { (menu: string): string, (menu: string, quantity: number): string, }; let order: FoodOrder = (menu: string, quantity?: number): string => { if (typeof quantity === "number") { return `${menu} ${quantity}개를 주문하겠습니다.`; } else { return `${menu} 먹고싶어요`; } }; console.log(order("자몽 허니 블랙티", 2)); //"자몽 허니 블랙티 2개를 주문하겠습니다." console.log(order("훠궈")); //"훠궈 먹고 싶어요"
이 코드는 FoodOrder라는 타입 별칭을 사용하여 음식 주문 함수의 타입을 정의합니다. 이 타입 별칭은 두 가지 형태의 함수 호출 시그니처를 가지고 있습니다. 첫 번째는 메뉴 이름만 문자열로 받아 문자열을 반환하는 함수를 나타내고, 두 번째는 메뉴 이름과 수량을 함께 받아 문자열을 반환하는 함수를 나타냅니다.그다음으로, FoodOrder 타입 별칭을 사용하여 order라는 함수의 타입을 지정합니다. 이 order 함수는 선택적 매개변수 quantity를 사용하여 두 가지 형태의 호출을 모두 처리할 수 있는 방식으로 구현되어 있습니다.
메뉴와 수량이 함께 주어지는 경우, 함수는 “${menu} ${quantity}개를 주문하겠습니다.” 라는 문자열을 반환합니다. 예를 들어, order("자몽 허니 블랙티", 2)를 호출하면 "자몽 허니 블랙티 2개를 주문하겠습니다."라는 메시지를 반환합니다.반면에 수량이 주어지지 않는 경우, 즉 메뉴만 주문하는 경우, 함수는 “${menu} 먹고 싶어요”라는 문자열을 반환합니다. 예를 들어 order("훠궈")를 호출하면 "훠궈 먹고 싶어요"라는 메시지를 반환합니다.

2) 인터페이스 예시

interface ReviewEvent { (menu: string): string; (menu: string, review: number): string; } let reviewEvent: ReviewEvent = (menu: string, review?: number): string => { if (typeof review === "number") { return `손님이 ${menu}을(를) 주문하고 리뷰 이벤트 ${review}번 품목을 선택했습니다.`; } else { return `손님이 ${menu}을(를) 주문했습니다.`; } }; console.log(reviewEvent("피자", 1)); //"손님이 피자을(를) 주문하고 리뷰 이벤트 1번 품목을 선택했습니다." console.log(reviewEvent("불족발")); //"손님이 불족발을(를) 주문했습니다."
이 코드는 ReviewEvent라는 인터페이스를 활용해 메뉴 주문과 리뷰 이벤트 선택을 처리하는 reviewEvent 함수를 정의하고 있습니다. 이 인터페이스는 두 가지 호출 시그니처를 가지며, 하나는 메뉴 이름만 받고, 다른 하나는 메뉴 이름과 리뷰 이벤트 번호를 함께 받습니다. 선택적 매개변수 review를 사용하여 reviewEvent 함수는 두 가지 호출 형태를 모두 처리할 수 있습니다.
메뉴와 리뷰 이벤트 번호가 함께 주어지는 경우, 함수는 “손님이 ${menu}을(를) 주문하고 리뷰 이벤트 ${review}번 품목을 선택했습니다.”라는 문자열을 반환합니다. 예를 들어, reviewEvent("피자", 1) 를 호출하면 “손님이 피자을(를) 주문하고 리뷰 이벤트 1번 품목을 선택했습니다.”라는 메시지를 반환합니다.
반면에 리뷰 이벤트 번호가 주어지지 않는 경우, “함수는 손님이 ${menu}을(를) 주문했습니다.”라는 문자열을 반환합니다. 예를 들어 reviewEvent("불족발") 을 호출하면 "손님이 불족발을(를) 주문했습니다."라는 메시지를 반환합니다.

5.4 함수 타입의 호환성

함수 타입의 호환성은 한 함수 타입이 다른 함수 타입으로 대체 가능한지를 판단하는 기준이며, 이는 반환값의 타입과 매개변수의 타입이 호환 여부에 따라 결정됩니다. 함수 간에는 상황에 따라 서로 대체가 가능한 경우와 그렇지 않은 경우가 있습니다. 이를 이해하기 위해서는 공변성과 반공변성을 알아둘 필요가 있습니다. 이에 따라, 이번 절에서는 타입의 호환성에 대한 이해를 돕기 위해 공변성과 반공변성에 대한 개념 먼저 살펴보도록 하겠습니다.

5.4.1 타입의 공변성과 반공변성, 이변성

타입스크립트에서는 타입 간의 관계를 설명하기 위해 공변성, 반공변성, 이변성 등의 개념을 사용합니다. A 타입과 B 타입이 있을 때, A가 B에 대입 가능한 경우, 이 관계를 A -> B로 표현할 수 있습니다. 이는 A가 B의 서브타입이고, B가 A의 슈퍼타입임을 의미합니다. 이렇게 정의된 A와 B에 대해, A 타입을 가진 타입 생성자를 T<A>, B 타입을 가진 타입 생성자를 T<B>로 표현할 수 있습니다. 다음으로 이러한 관계성을 바탕으로 더 자세히 알아보겠습니다.

1) 공변성

공변성(Covariance)이란 A가 B의 서브타입일 때, T<A>가 T<B>에 대입 가능하다는 것을 의미합니다. 이는 T<A>가 T<B>의 서브타입임을 나타내고, T<A> -> T<B>로 표현할 수 있습니다. 예를 들어, T<A>를Array<string> 로 나타낼 수 있습니다. 또한 공변성은 strict 옵션과 관계없이 함수의 반환값에 대해서 항상 적용됩니다. 다음 예시를 통해 더 자세히 알아보겠습니다.
let A: Array<string> = []; // 서브 타입 let B: Array<string | number> = [];// 슈퍼 타입 B = A; // Ok A = B; // Error: '(string | number)[]' 형식은 'string[]' 형식에 할당할 수 없습니다.
위의 예제에서 string은 string | number의 서브타입이므로, Array<string>는 Array<string | number>의 서브타입이 됩니다. 이를 통해 A 타입은 B 타입에 할당될 수 있습니다. 반대로, B 타입은 A 타입에 비해 number 원소를 포함하므로, 서브 타입인 A에 할당하게 되면 A 타입은 number 원소를 처리할 수 없기 때문에 타입 에러가 발생합니다. 따라서, 타입스크립트는 서브 타입을 슈퍼 타입에 할당하는 공변성을 기본적으로 가지고 있다는 것을 알 수 있습니다.

2) 반공변성

반공변성(Contravariance)이란, A가 B의 서브타입일 때 T<B>를 T<A>에 대입할 수 있음을 의미합니다. 따라서 T<B>는 T<A>의 서브타입이고, 이 관계를 T<B> -> T<A>로 표현할 수 있습니다. 이 개념을 좀 더 명확히 이해하기 위해, T<A>를 타입<매개변수>로 나타낼 수 있습니다.
또한, 타입스크립트는 기본적으로 공변성을 가지지만, 함수의 매개변수는 예외적으로 반공변성을 가집니다. 이는 strictFunctionTypes 옵션과 strict 옵션이 tsconfig.json 파일에서 모두 true로 설정되어 있을 때만 적용됩니다. 만약 이 두 옵션이 false로 설정되어 있다면, 함수의 매개변수는 이변성을 가지게 됩니다.다음으로 A 함수와 B 함수의 매개변수 타입에 대해 살펴보겠습니다.
type T<Param> = (param: Param) => void; let A: T<number> = (param) => { // 서브타입 console.log(param); }; let B: T<string | number> = (param) => { // 슈퍼타입 console.log(param); }; A = B; // OK B = A; // Error: 'T<number>' 형식은 'T<string | number>' 형식에 할당할 수 없습니다.
위의 예제에서 제네릭 타입 T<Param>을 받는 두 개의 콜백 함수 A와 B가 있습니다. A 함수는 number를 매개변수로 받고, B 함수는 string 또는 number를 받습니다. AB보다 좁은 범위의 매개변수 타입을 받기 때문에, AB의 서브타입이 됩니다. 따라서, 슈퍼타입인 B를 서브타입인 A에 할당하는 것이 가능합니다. 반대로 AB에 할당하려고 하면, A가 받는 매개변수 타입이 B의 매개변수 타입의 서브타입이기 때문에 에러가 발생합니다.

3) 이변성

이변성(Bivariance)은 공변성과 반공변성을 동시에 가지는 경우를 말합니다. 즉, T<A>가 T<B>에 대입 가능한 동시에 T<B>가 T<A>에 대입 가능한 상황을 의미합니다. 이를 이해하기 위해, tsconfig.json 파일에서 strict 옵션과 strictFunctionTypes 옵션을 모두 false로 설정해야 합니다. 다음으로 이변성과 반공변성의 차이를 이해하기 위해, 이전에 다룬 코드를 다시 살펴보겠습니다.
type T<Param> = (param: Param) => void; let A: T<number> = (param) => { console.log(param); }; let B: T<string | number> = (param) => { console.log(param); }; A = B; // OK B = A; // OK??
다음과 같이 정리할 수 있습니다.
개념
strict 옵션 여부
공변성
A가 B의 서브타입일 때, T<A>는 T<B> 의 서브타입인 경우
관계없이 적용
반공변성
A가 B의 서브타입일 때, T<B>는 T<A> 의 서브타입인 경우
true일 때 적용
이변성
A가 B의 서브타입일 때 , T<A>는 T<B> 의 서브타입도 가능하고, T<B>는 T<A> 의 서브타입도 가능 한 경우
false일 때 적용
이 관계를 바탕으로 함수 타입의 호환성에 대해 알아보도록 하겠습니다.

5.4.2 반환값의 타입에 따른 호환성

두 함수의 타입이 있다고 가정할 때, 함수의 반환값 타입은 공변성의 특성에 따라 호환 가능한지 판단하게 됩니다. 아래 예시를 통해 확인해 보겠습니다.
type Novel = () => "Harry Potter"; // 서브타입 type Book = () => string; // 슈퍼타입 let novelFunc: Novel = () => "Harry Potter"; let bookFunc: Book = () => "Book"; bookFunc = novelFunc; // OK novelFunc = bookFunc; // Error: 'Book' 형식은 'Novel' 형식에 할당할 수 없습니다.
위의 예제에서 novelFunc 함수의 반환 값 타입 NovelbookFunc 함수의 반환 값 타입 Book의 서브타입입니다. 따라서 공변성의 특성에 따라, novelFuncbookFunc에 할당하는 것이 가능합니다. 반대로 bookFuncnovelFunc에 할당하려고 하면 에러가 발생합니다. 이는 bookFunc의 반환 값 타입 BooknovelFunc의 반환 값 타입 Novel의 서브타입이 아니기 때문입니다.

5.4.3 매개변수의 타입에 따른 호환성

다음으로 함수의 매개변수가 호환되는지 확인하려면, 함수의 매개변수 개수가 같은지 또는 다른지에 따라 상황을 따져봐야 합니다.

1) 매개변수의 개수가 같은 경우

함수의 매개변수 개수가 동일한 경우, 매개변수의 타입은 서로 반공변성을 가져야 호환이 가능합니다. 이를 확인해 보기 위해 tsconfig.json 파일의 strict 옵션을 true로 설정하고 아래의 코드를 살펴보도록 하겠습니다.
type Novel = (title: "Harry Potter") => void; // 서브타입 type Book = (title: string) => void; // 슈퍼타입 let novelFunc: Novel = (title) => { /*..*/ }; let bookFunc: Book = (title) => { /*..*/ }; novelFunc = bookFunc; // OK bookFunc = novelFunc; // Error: 'Novel' 형식은 'Book' 형식에 할당할 수 없습니다.
위의 예제에서 novelFunc의 매개변수 타입인 NovelbookFunc의 매개변수 타입인 Book의 서브타입입니다. 따라서 반공변성의 특징에 따라, bookFunc 함수를 novelFunc에 할당하는 것이 가능합니다. 반대로novelFunc 함수를 bookFunc 함수에 할당하면, novelFunc의 매개변수 타입 NovelbookFunc의 매개변수 타입 Book의 슈퍼타입이 아니기 때문에 에러가 발생합니다. 이번엔 객체 타입을 함수의 매개변수로 사용하는 경우를 살펴보겠습니다.
interface Beverage { // 슈퍼타입 name: string; size: string; } interface Coffee extends Beverage { // 서브타입 kind: string; } let orderBeverage = (beverage: Beverage) => console.log( `손님이 ${beverage.size} 사이즈의 ${beverage.name}를 주문하셨습니다.` ); let orderCoffee = (coffee: Coffee) => console.log( `손님이 ${coffee.size} 사이즈의 ${coffee.kind} 커피를 주문하셨습니다.` ); orderCoffee = orderBeverage; // Ok orderBeverage = orderCoffee; // Error: 'coffee' 및 'beverage' 매개 변수의 형식이 호환되지 않습니다.
위의 예제에서 Beverage 타입은 Coffee 타입의 슈퍼타입이므로, 반공변성의 특성에 따라 Beverage를 매개변수로 받는 orderBeverage 함수는 Coffee를 매개변수로 받는 orderCoffee 함수에 할당할 수 있습니다. 하지만 orderCoffee 함수는 Beverage를 매개변수로 받는 orderBeverage 함수에 할당할 수 없습니다. 이는 Coffee에만 존재하는 kind 프로퍼티가 Beverage에는 없기 때문입니다. 만약 orderCoffee 함수를 orderBeverage 함수에 할당할 수 있었다면, 아래와 같은 상황이 발생할 수 있습니다.
orderBeverage = orderCoffee; // 컴파일 에러가 발생하지 않습니다. orderBeverage({ name: "아메리카노", size: "Tall" }); // "손님이 Tall 사이즈의 undefined 커피를 주문하셨습니다."
위의 예제에서 orderBeverage({ name: "아메리카노", size: "Tall" })를 실행한다면, orderBeverage 함수가 orderCoffee 함수로 할당되었기 때문에 Beverage 타입을 받는 orderBeverage 함수에 kind 프로퍼티가 없어서 undefined가 출력됩니다.

2) 매개변수의 개수가 다른 경우

두 함수의 매개변수 개수가 다를 경우, 매개변수가 적은 쪽이 많은 쪽에 할당할 수 있습니다. 이때 사용되지 않는 매개변수는 무시됩니다.
type LoginProp = (email: string, name: string) => void; type ResetPasswordProp = (email: string) => void; let login: LoginProp = (email, name) => { console.log(`안녕하세요!, ${name} 님! ${email} 계정으로 로그인하셨습니다.`); }; let resetPassword: ResetPasswordProp = (email) => { console.log(`${email}으로 비밀번호 재설정 메일이 전송되었습니다.`); }; login("hayeon@test.com", "이하연"); // "안녕하세요!, 이하연 님! hayeon@test.com 계정으로 로그인하셨습니다." login = resetPassword; // Ok login("hayeon@test.com", "이하연"); // "hayeon@test.com으로 비밀번호 재설정 메일이 전송되었습니다."
위의 예제에서 정의한 LoginProp 타입은 email과 name 이라는 두 개의 문자열 매개변수를 받는 함수 타입이고, ResetPasswordProp 타입은 email 이라는 한 개의 문자열 매개변수를 받는 함수 타입입니다.
따라서 resetPassword 함수가 login함수에서 필요로 하는 첫 번째 매개변수인 email을 가지고 있기 때문에 resetPassword 함수를 login 함수에 할당할 수 있습니다. 이 경우 login 함수의 두 번째 매개변수인 name은 무시되게 됩니다.
그 결과 login("hayeon@test.com", "이하연")를 실행하면 초과 매개변수는 제외되고, "hayeon@test.com으로 비밀번호 재설정 메일이 전송되었습니다."로 출력되는 것을 확인할 수 있습니다. 즉, 할당하는 함수의 매개변수 수 더 적더라도, 매개변수의 타입이 일치하면 할당이 가능합니다. 반대로 login 함수를 resetPassword 함수에 할당하면, 아래와 같이 에러가 발생합니다.
resetPassword = login; // Error: 'LoginProp' 형식은 'ResetPasswordProp' 형식에 할당할 수 없습니다. resetPassword("hayeon@test.com"); // "안녕하세요!, undefined 님! hayeon@test.com 계정으로 로그인하셨습니다."
위의 예제에서 login 함수는 resetPassword 함수에 비해 매개변수가 하나 더 많습니다. 따라서 resetPassword 함수에 login 함수를 할당하려 할 때, login 함수의 두 번째 매개변수인 name에 대응하는 값이 없어 에러가 발생합니다. 만약 이러한 할당이 허용된다면, resetPassword("hayeon@test.com")를 실행시켰을 때, login 함수의 두 번째 매개변수 name으로 undefined 값이 전달되게 됩니다. 즉, 타입스크립트는 함수의 매개변수 개수를 고려하여 타입 호환성을 검사합니다. 이를 통해 런타임에서 발생할 수 있는 예상치 못한 오류를 미리 방지할 수 있습니다.