[Typescript] - (4) Typescript 함수
[함수 타입]
함수의 타입
일반적으로 함수를 설명할 때 JS에서는 매개변수가 무엇이고 결과값이 무엇인지에 대한 정보가 중요하다.
하지만 TS에서는 매개변수와 결과값도 중요하지만 각 값들의 타입이 무엇인지에 대한 정보도 중요하다.
//함수 선언문
function func(a:number,b:number) : number {
return a + b;
}
//화살표 함수
const add = (a:number, b:number) : number => a + b;
위의 코드와 같은 방식으로 매개변수와 결과값의 타입을 지정해주는데, return문이나 "=>"가 있는 경우 매개변수의 타입을 보고 TS 자체적으로 결과값의 타입을 추론하기도 한다.
함수의 매개변수
매개변수의 타입을 정의해줄 경우 기본값을 설정해둘 수 있다. 하지만 기본값을 기준으로 추론된 타입과 그 매개변수의 타입을 다르게 정의하면 오류가 발생한다. 더해서, 매개변수로 정의되어 있지 않은 타입을 인수로 받아 함수를 실행하면 역시나 오류가 발생한다.
function func(name="Tom"){
console.log(`name: ${name}`)
}
func("Bob");
func(10); //Error
function func(name:number="Tom"){ //Error
console.log(`name: ${name}`)
}
매개변수로 정의는 해두었지만 함수를 실행할 때 필수로 넣어야하는 매개변수가 아니라면 Optional Parameter(선택적 매개변수)를 사용하면 된다. 앞서 보았던 Optional Property(선택적 프로퍼티)와 같이 JS의 Optional Chaining 방식으로 적용되는 문법이며 선택적 매개변수로 정의된 매개변수는 타입 추론시 Union Type으로 추론된다. 따라서 해당 매개변수를 함수 내에서 사용할 때에는 Type Guard를 이용해서 조건으로 처리를 해주어야 한다.
function func(name="tom", age:number){
console.log(`name: ${name}`);
console.log(`age: ${age}`);
}
func("bob",10);
func("bob"); //Error, There are two parameters
function func(name="tom", age?:number){
console.log(`name: ${name}`);
console.log(`age: ${age - 5}`); //Error, typeof age can be undefined
}
function func(name="tom", age?:number){
console.log(`name: ${name}`);
if(typeof age === "number"){
console.log(`age: ${age - 5}`);
}
}
Optional Parameter(선택적 매개변수)는 반드시 Essential Parameter(필수 매개변수)보다 뒤에 위치해야한다.
Rest Parameter(나머지 매개변수)
가변적인 길이의 인수들을 배열로 묶어서 rest 변수에 할당하여 전달하는 JS내의 문법이다. Rest Parameter로 매개변수를 받을 시에는 배열 형태로 받으면 된다. 함수 실행 시 받을 인수의 타입에 맞게 배열 타입을 정의해주면 나머지 매개변수로 받을 수 있다. 또한 Tuple Type을 통해 받을 매개변수의 개수를 설정할 수도 있다. Rest Parameter 자체가 배열이기 때문에 Tuple Type으로 정의가 가능한 것이다.
function func(...rest: number[]){
let sum = 0;
rest.forEach(e => sum += it);
return sum;
}
func(1,2,3,4,5); //15
func(1,2,3); //6
------------------------------------------------
//Rest Parameter + Tuple
function func(...rest: [number,number,number]){
let sum = 0;
rest.forEach(e => sum += it);
return sum;
}
func(1,2,3,4,5); //Error
func(1,2,3); //6
[함수 타입 표현식과 호출 시그니쳐]
함수 타입 표현식(Function Type Expression)
앞에서 살펴보았던 Type Alias(타입 별칭) 기법을 함수에서 사용하는 방식이다. 함수 내에 타입을 여럿 정의하면 코드의 길이도 길어질뿐만 아니라 서로 다른 함수가 같은 타입의 매개변수와 반환값을 갖는 경우 범용적으로 사용이 가능해서 유용한 방식이다. 사용 방식은 아래 코드와 같다.
//1. Type Alias
type Operation = (a:number, b: number) => number;
const add:Operation = (a,b) => a + b;
const sub:Operation = (a,b) => a - b;
const multi:Operation = (a,b) => a * b;
const divide:Operation = (a,b) => a / b;
//2. Literal Type
const add:(a:number, b: number) => number = (a,b) => a + b;
const sub:(a:number, b: number) => numbern = (a,b) => a - b;
const multi:(a:number, b: number) => number = (a,b) => a * b;
const divide:(a:number, b: number) => number = (a,b) => a / b;
함수 타입 표현식을 사용하게 되면 매개변수로 정의된 타입 및 개수에 맞춰서 함수에 인수로 담고 실행해야 한다는 점을 주의해야한다.
호출 시그니쳐(Call Signiture)
함수 타입 표현식과 마찬가지로 Type Alias 기법을 사용한 방식으로 함수판 Type Alias라고 보면 된다. JS 문법에서 다루어 보았듯이 함수는 객체의 일종이기 때문에 객체 형태로 타입을 지정하는 방식이다. 타입 정의 방식부터 코드로 보자.
type Operaion = {
(a:number, b:number) : number
}
const add: Operation2 = (a,b) => a + b;
const sub: Operation2 = (a,b) => a - b;
또한 하이브리드 타입이라고 해서 호출 시그니쳐 기법으로 정의된 타입에 프로퍼티를 추가로 입력하여 활용하는 타입이 있다. 사용에 추천은 하지 않지만 간단하게 형태만 알아보도록 하자.
type Operaion = {
(a:number,b:number) : number;
name: string
}
const add:Operation = (a,b) => a + b;
add(1,2);
add.name = "Tom" //Not Error
[함수 타입의 호환성]
함수 타입 호환성
- 특정 함수 타입을 다른 함수 타입으로 취급해도 되는지 여부를 판단하는 것이다. 서로 다른 타입의 함수가 있을 때 함수 간의 호환성을 따져본다고 볼 수 있다. 호환성 확인에는 크게 두가지로 나뉜다.
1. 반환값의 타입 호환성
- 반환값의 타입 호환성은 반환값이 Upcasting 되는 경우 호환된다.
type A = () => number; //number Type
type B = () => 10; //number literal type
let a:A = () => 10;
let b:B = () => 10;
a = b;
b = a; //Error
위의 코드에서 보면 Number Type은 Number Literal Type의 Super Type이기 때문에 변수 b를 변수 a로 대입하는 것은 Upcasting이라 가능하지만 그 반대는 Downcasting이기 때문에 불가능하다.
2. 매개변수의 타입 호환성
2-1. 매개변수의 개수가 같은 경우
- 매개변수가 Downcasting 되는 경우 호환된다.
type C = (value:number) => void; //parameter -> Number Type
type D = (value:10) => void; // parameter -> Number Literal Type
let c:C = (value) => {};
let d:D = (value) => {};
c = d; //Error
d = c;
---------------------------------------------
//Object
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; //Error
dogFunc = animalFunc;
let testFunc = (animal:Animal) => {
console.log(animal.name);
console.log(animal.color); //Error, Animal Type doesn't have color property
}
let testFunc1 = (dog:Dog) => {
console.log(dog.name);
}
위의 코드를 보았을 때 함수의 경우 변수 c의 매개변수는 Number Type, 변수 d의 매개변수는 Number Literal Type이다. 따라서 변수 d를 변수 c에 대입하는 경우 Upcasting이 되기 때문에 오류가 발생하고, 변수 c를 변수 d에 대입하는 경우 Downcasting이 되기 때문에 호환이 가능한 것이다.
그 아래 객체 형태의 Type Alias와 그에 맞게 생성된 함수를 보면 Animal 타입은 Dog 타입보다 갖고 있는 속성의 개수가 적기 때문에 Animal 타입은 Dog 타입의 슈퍼 타입이 된다. 우리가 이해하고 있던 타입 호환성에 따르면 Dog 타입으로 정의된 dogFunc를 Animal 타입으로 정의된 animalFunc에 대입하였을 때 Upcasting이라 가능할 것 같지만 함수의 매개변수 호환성에서는 반대로 Downcasting이 되어야 호환이 된다.
2-2. 매개변수의 개수가 다른 경우
- 매개변수의 개수가 더 적은 쪽이 더 많은 쪽으로 호환 가능하다.
type Func1 = (a: string,b:number) => void;
type Func2 = (a: string) => void;
let func1:Func1 = (a,b) => {};
let func2:Func2 = (a) => {};
func1 = func2;
func2 = func1; //Error
------------------------------------------
type Func1 = (a: number,b:number) => void;
type Func2 = (a: string) => void;
let func1:Func1 = (a,b) => {};
let func2:Func2 = (a) => {};
func1 = func2; //Error
func2 = func1; //Error
위의 코드를 보면 Func1 타입으로 정의된 func1의 매개변수는 2개, Func2 타입으로 정의된 func2의 매개변수는 1개이다. 간단하게 생각해서 매개변수의 개수가 더 적다면 호환이 되지 않는다. 따라서 func2에 func1을 대입하려고 하면 매개변수의 개수가 더 적어지게 되므로 오류가 발생하는 것이다.
여기서 주의해야할 점은 개수가 다르더라도 호환이 되기 위해서는 같은 매개변수의 타입은 동일해야 한다는 점이다. 애초에 매개변수의 타입이 모두 다르다면 호환 자체가 불가능해진다.
[함수 오버로딩]
함수 오버로딩
- 함수를 매개변수의 개수나 타입에 따라 여러가지 버전으로 정의하는 방법이다. 함수의 다양한 버전을 정의하는 Overoad Signature(오버로드 시그니쳐)가 있고 실제로 구현을 하는 Implement signature(구현 시그니쳐)가 있다. 코드를 작성해보며 살펴보자.
//Overoad signature
function func(a:number):void;
function func(a:number, b:number, c:number):void;
//Implement Signature
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);
}
}
위의 코드를 보면 매개변수가 1개일 경우에는 20을 곱해주고 3개인 경우에는 모든 매개변수의 합을 반환하는 함수를 작성하였다. 매개변수의 개수에 따른 버전을 나누어두고 함수를 실행하는 것이다. 매개변수의 타입에 따라서도 버전을 나누어 사용할 수 있다. 함수 오버로딩은 다양한 라이브러리를 사용할 때 유용하다.
[사용자 정의 타입 가드]
사용자 정의 타입 가드(Custom Type Guard)
- 말그대로 사용자의 입맛에 맞게 타입 가드를 설정하는 기법이다. 간단한 코드를 보면서 이해하는 것이 쉬울 것 같다.
type Dog = {
name: string,
isBark: boolean
}
type Cat = {
name: string,
isScratch: boolean
}
type Animal = Dog | Cat;
function warning(animal:Animal){
if("isBark" in animal){
animal;
}else if("isScratch" in animal){
animal;
}
}
위의 코드는 Type Alias로 정의된 타입의 property를 갖고 Type Guard를 생성하여 타입 좁히기를 통해 특정 기능을 수행하는 과정이다. 하지만 앞에서 공부했듯이 특정 속성명을 갖고 타입 좁히기를 하면 코드의 직관성이 떨어지고 해당 속성이 어느 Type의 속성인지 찾아봐야하는 번거로움도 발생한다. 또한 외부 라이브러리를 사용하는 경우에는 속성명이 변경되기라도 하면 타입 좁히기에 오류가 발생할 가능성이 높아진다. 이 부분을 방지하기 위해 사용자가 직접 Type Guard를 만들어서 사용하는 것이다.
type Dog = {
name: string,
isBark: boolean
}
type Cat = {
name: string,
isScratch: boolean
}
type Animal = Dog | Cat;
function isDog(animal:Animal): animal is Dog{
return (animal as Dog).isBark !== undefined
}
function isCat(animal:Animal) : animal is Cat{
return (animal as Cat).isScratch !== undefined
}
function warning(animal:Animal){
if(isDog(animal)){
animal;
}else if("isScratch" in animal){
animal;
}
}
isDog와 isCat이라는 함수를 통해서 Type Guard를 생성하였다. 각 함수는 매개변수의 타입이 Union Type으로 정의된 Animal Type을 받고 있다. 하지만 TS에서는 사용자가 직접 만든 함수의 반환값을 갖고는 타입 좁히기가 실행되지 않는다. 따라서 반환값의 타입을 정의해주면서 Custom Type Guard를 생성하게 되는것이다. "animal is Dog/Cat"으로 반환값의 타입을 Type Alias로 정의해둔 타입이라고 정의해주면 warning 함수 내부의 조건문에서 자동으로 타입 좁히기를 실행하여 원하는 타입으로 사용이 가능하게 된다.