...
타입스크립트 함수 표현
타입스크립트에서 함수를 표현하는 방법은 머리 아플 정도로 여러가지이다.
내가 쓰기 편한 함수 선언 방식을 이용하면 되겠지만, 남이 만든 라이브러리나 코드를 보기 위해서는 어떤 타입스크립트 함수 표현식이 있는지 알 필요가 있다.
일반적인 함수 정의
자바스크립트에서 함수를 표현하는데 크게 3가지 표현식이 있다.
여기에 그냥 매개변수와 리턴값만 타입만 지정해주면 되니 그렇게 난해하지는 않다.
함수 선언식
//* 함수 선언식
function myFunc1(x: number, y: number): number {
return x + y;
}
함수 표현식
//* 함수 표현식
let myFunc2 = function (x: number, y: number): number {
return x + y;
};
화살표 함수
//* 화살표 함수
let myFunc3 = (x: number, y: number): number => {
return x + y;
};
Call Signature (함수 타입)
여기서 부터 문제이다.
문자열을 string, 정수배열을 number[] 로 표현했듯이 함수 역시 함수를 표현하는 타입을 미리 선언 가능하다.
함수 타입을 미리 선언하고 뒤에 함수식을 붙여넣으면, 함수 아규먼트에서 타입을 또 선언하지 않아도 된다는 특징이 있다.
//* 변수에 미리 함수 타입을 지정
let myFunc4: (arg1: number, arg2: number) => number;
myFunc4 = function (x, y) {
return x + y;
}; // 미리 변수에 함수 타입을 지정했기에 대입하는 함수식에 타입을 쓰지않아도 된다.
//* 위의 과정을 한줄로 표현
let myFunc5: (arg1: number, arg2: number) => number = (x, y) => {
return x + y;
};
여기서 조심해야 되는게 화살표가 있다고해서 위의 화살표 함수와 햇깔리지 말아야 한다는 점이다.
위에서 함수 표현식에선 함수 리턴 타입을 콜론(:) 으로 표현했는데 따로 함수 타입을 선언할때는 화살표로 리턴 타입을 표현하니 미간에 주름이 잡히겠지만 익숙해져야 한다...
type 별칭 및 인터페이스 역시 개별적으로 함수타입을 선언하여 사용될 수 있다.
인터페이스는 함수 타입을 또 화살표가 아닌 콜론(:)으로 표현하니 미치고 팔딱 뛸 노릇이다.
//* type 리터럴로 함수 타입을 지정
type Add1 = (x: number, y: number) => number;
let myFunc6: Add1 = (x, y) => {
return x + y;
};
//* 인터페이스로 함수 타입을 지정
interface Add2 {
(x: number, y: number): number;
}
let myFunc7: Add2 = (x, y) => {
return x + y;
};
가끔 어떤 라이브러리에는 다음과 같이 해괴망측한 타입 함수 표현을 하기도 한다.
이에 대해선 뒤에 함수 오버로딩 챕터에서 자세히 다룬다.
function myFunc8(x: string, y: string): string; // 함수 타입 선언
function myFunc8(x: number, y: number): number; // 함수 타입 선언
function myFunc8(x: any, y: any) { // 실제 함수 선언
return x + y;
}
myFunc8(1, 2);
타입스크립트 매개변수 표현
타입스크립트에서는 함수의 인자를 모두 필수 값으로 간주한다.
따라서, 함수의 매개변수를 설정하면 심지어 인자값이 undefined나 null 같은 쓸모없는 값이이라도 인자로 넘겨야하며, 컴파일러에서는 정의된 매개변수 값이 넘어 왔는지 확인한다.
function sum(a: number, b: number): number {
return a + b;
}
sum(10, 20); // 30
sum(10, 20, 30); // error, too many parameters
선택적 매개변수
만일 자바스크립트 처럼 유연하게 정의된 매개변수의 갯수 만큼 인자를 넘기지 않아도 되게 만들고 싶다면, 선택적(optional) 키워드인 물음표(?)를 이용해서 아래와 같이 정의할 수 있다.
다음 예제를 보면 ? 키워드를 사용해 b를 선택적 매개 변수로 지정했다. 따라서 b가 받을 인수가 없어도 에러가 발생하지 않는다.
function sum(a: number, b?: number): number {
return a + b;
}
sum(10, 20); // 30
sum(10); // 타입 에러 없음
사실 위 예제는 정확히 다음 예제와 같다. 즉, ? 키워드 사용은 유니온 타입 | undefined 를 추가하는 것과 같다고 볼 수 있다.
function sum(a: number, b: number | undefined): number {
return a + b;
}
sum(10, 20); // 30
sum(10); // 타입 에러 없음
좀더 응용하자면 다음과 같이 구성할 수 있다.
함수 호출에서 두 번째 매개변수와 세 번째 매개변수에 값을 만일 전달하지 않으면, 선택적 프로퍼티(?) 에 의해 에러는 안나지만, undefined로 할당되게 된다.
그래서 Null 병합 연산자( ?? )를 사용하여 null 또는 undefined인 경우 0을 반환하도록 지정하는 식으로 처리가 가능하다.
function add(a: number, b?: number, c?: number): number {
return a + (b ?? 0) + (c ?? 0);
}
add(1, 2, 3); // 6
add(1, 2); // 3
add(1); // 1
단, 선택적 매개변수를 사용할때 주의할점은 선택적 매개변수가 이외의 함수 인자 앞으로 위치하면 안되는 것이다. (선택적 매개변수는 무조건 뒤로 위치)
function sum(b?: number, a: number): number {
return a + (b ?? 0);
}
만일 옵셔널 매개변수를 앞에다 꼭 두고 사용하고 싶다면, 같은 타입 동작인 유니온 타입 ( number | undefined )으로 선언하면 된다. 다만 사용성이나 가독성이 그리 좋지는 않아 이러한 기법은 지양하는 편이다.
function sum(b: number | undefined, a: number): number {
return a + (b ?? 0);
}
sum(20, 11); // 31
sum(undefined, 11); // 11
매개변수 초기화
매개변수 초기화는 자바스크립트 ES6 문법과 동일하다.
기본값을 할당한 매개변수의 함수 타입을 보면 옵셔널 파라미터(선택적 매개변수)가 적용된 걸 확인 할 수 있다.
왜냐하면 매개변수에 기본 값이 있으면 매개변수에 값을 할당 하지 않아도 된다는 의미가 되기 때문이다.
더불어 기본 값이 있으면 굳이 인자 타입을 선언하지 않아도 된다. 타입스크립트에선 이를 타입 추론이라 칭한다.
function sum(a: number, b = 100): number { // 매개변수 기본 값이 있으면 굳이 인자 타입을 선언하지 않아도 된다 (타입 추론)
return a + b;
}
sum(10, undefined); // 110
sum(10); // 110
sum(10, 10) // 20
나머지(rest) 매개변수
ES6에서 사용되는 스프레드 매개변수 역시 직접 타입만 잘 지정하면 타입스크립트에서도 문제없이 사용가능하다.
function sum(a: number, ...nums: number[]): number {
const totalOfNums = 0;
for (let key in nums) {
totalOfNums += nums[key];
}
return a + totalOfNums;
}
다만, 타입스크립트에서 strict 모드를 true로 하고 사용하기 때문에, 자바스크립트의 arguments 예약어는 사용될수 없다는 점은 유의해야 한다.
네임드 파라미터
named parameters는 함수 아규먼트 부분 전체를 객체로 감싸주고, 그 뒤에 타입을 정의를 해주는 방식이다.
마치 자바스크립트의 구조분해 문법을 타입으로 구현한 것이라고 할 수 있다.
function getText({ name, age = 15, language }: { name: string; age?: number; language?: string }): string {
const nameText = name.substr(0, 10);
const ageText = age >= 35 ? 'senior' : 'junior';
return `name: ${nameText}, age: ${ageText}, language: ${language}`;
}
getText({ name: '홍길동', age: 11, language: 'kor' });
getText({ name: '홍길동' });
우리 독자분들은 위의 코드를 보며 '별 해괴한 문법이 다있어' 🤢 라며 손사래를 치시겠지만, 네임드 파라미터는 함수의 아규먼트 갯수가 많을때 유용히 사용될 수 있다.
왜냐하면 아규먼트의 타입 정보를 뒤에다 내뺌으로써, 한눈에 함수 아규먼트 구성을 볼수 있기 때문이다.
변환 방법도 에디터 내에서 자동으로 리팩토링을 제공하기 때문에 어렵지 않다.
다만 그래도 이러한 함수 표현방식이 도저히 익숙하지 않는다면 굳이 네임드 파라미터를 이용할 필요는 없다.
타입스크립트 콜백 / 중첩 / 고차 함수
콜백 함수
콜백 인자의 타입을, 위에서 배운 call signiture(함수 타입)으로 정의하면 된다.
const logging = function (s: string) {
console.log(s);
};
const init = (callback: (arg: string) => void) => {
console.log('callback start');
callback('yes!');
console.log('callback end');
};
init(logging);
/*
callback start
yes!
callback end
*/
중첩 함수
큰 제약없이 역시 함수 타입 선언만 잘 해놓으면 간단히 중첩해서 사용이 가능하다.
const calc = (value: number, cb: (arg: number) => void) => {
const add = (a: number, b: number) => a + b; // 화살표 함수
function mul(a: number, b: number) { // 함수 표현식
return a * b;
}
let result = mul(add(3, 4), value);
cb(result);
};
calc(2, (output) => console.log(output)); // 14
고차 함수
고차 함수(high order function)란 함수를 반환하는 함수를 말한다.
const add = (a: number) => {
return (b: number) => {
return a + b;
};
};
// 곧바로 실행
console.log(add(3)(8)); // 11
// 한 번 걸쳤다가 실행
const first = add(3);
console.log(first(8)); // 11
타입스크립트 this 표현
자바스크립트에서도 그랬듯이, 함수를 다루는데 있어 가장 중요한 내용 중 하나가 바로 this 키워드 이다.
자바스크립트 함수 내 this는 전역 객체를 참조하거나, undefined가 되는 등 우리가 원하는 콘텍스트(context)를 잃고 다른 값이 되는 경우들이 있는 경우가 비일비재 하기 때문이다.
타입스크립트의 this 타입 표현을 학습하기에 앞서, 자바스크립트의 this를 한번 더 복습하고 오는걸 추천한다.
명시적 this
다음과 같이 someFn 메소드는 전역 렉시컬 스코프에 위치하기에 this에 any오류가 나타나게 된다.
interface Cat {
name: string;
}
const cat: Cat = {
name: 'Lucy',
};
function someFn(greeting: string) {
console.log(`${greeting} ${this.name}`); // Error - 'this'에는 형식 주석이 없으므로 암시적으로 'any' 형식이 포함됩니다.
}
someFn.call(cat, 'Hello'); // 위에 에러가 나서 실행이 안됨
이 경우 this의 타입을 명시적으로(explicitly) 선언할 수 있다.
someFn 이라는 메소드의 this의 타입은 명시적으로 ICat 인터페이스를 가리키게 함으로서 타입을 명시해 오류를 없앤다.
다음과 같이 첫 번째 가짜(fake) 매개변수로 this를 선언하면 된다.
interface ICat {
name: string
}
const cat: ICat = {
name: 'Lucy'
};
function someFn(this: ICat, greeting: string) {
console.log(`${greeting} ${this.name}`); // ok
}
someFn.call(cat, 'Hello'); // Hello Lucy
타입스크립트 함수 오버로딩
오버로딩이란 매개변수가 다른 동일한 함수를 말하는 것이다. 자바나 C#을 하시다 온 분들이라면 익숙한 단어일 것이다.
타입스크립트의 함수 오버로드(Overloads) 역시 이름은 같지만, 정확히 말하자면 매개변수 타입과 반환 타입이 다른 여러 함수를 가질 수 있는 것을 말한다.
함수 오버로드를 통해 다양한 구조의 함수를 생성하고 관리할 수 있어 유용하게 쓰인다.
함수 오버로드 선언법은 약간 독특한데 중괄호 {} 없는 함수를 실제 함수 위에다가 써주면 된다.
function add(a: string, b: string): string; // 오버로드 함수 선언
function add(a: number, b: number): number; // 오버로드 함수 선언
function add(a: any, b: any): any { // 오버로드 함수 실행부 (any를 써준다)
return a + b;
}
add('hello ', 'world~');
add(1, 2);
Q. 타입스크립트에선 any를 쓰지말라 했는데 오버로드 함수에는 써도 문제없을까?
결론부터 말하자면 전혀 문제가 없다.
오버로드 함수 실행부 에서의 인수와 리턴 타입에 any를 적어주었는데, 사실 컴파일러는 오버로드 함수 선언부의 타입들만 보고 함수를 판단하기에 실행부에 any를 썼다고 해서 문제가 되지 않는다.
실제로add([1, 2], 3)와 같이 오버로드 함수의 a 인수 타입에 정의되지않은 데이터를 넣을 경우 컴파일러 에러가 나타나게 된다. (이는 any 타입이 작동하지 않는다는 말이다)
따라서 '오버로드 함수에서의 any 표현은 타입스크립트에서 허용된다'라고 이해하면 된다.
다음과 같은 함수가 있다고 가정하자.
age의 값이 number냐 string 이냐에 따라 조건 분기가 갈린다. 그리고 return 값이 객체와 문자열 두개이기 때문에 함수 리턴 타입 역시 유니온으로 지정했다.
interface User {
name: string;
age: number;
}
function join(name: string, age: number | string): User | string {
if (typeof age === 'number') {
return { name, age };
} else {
return '나이는 숫자로 입력해 주세요';
}
}
이 코드는 어뜻보기에는 타입도 잘 명시되어 있고 라인 에러도 안떠서 옳은 코드인줄 알겠지만 실제로 함수를 실행시켜보면 에러가 발생하게 된다.
컴파일러가 이렇게 경고를 주는 이유는, 컴파일러 입장에서 join 함수가 User를 반환할지 string을 반환할지 확신이 없기 때문이다.
코드 상에서는 분명 if 문으로 분기처리가 잘 되어있지만, 타입만 놓고 봤을때는 확신이 없기 때문에 컴파일러가 에러를 주는 것이다.
이럴때 바로 함수 오버로딩을 통해 해결할 수 있다.
interface User {
name: string;
age: number;
}
function join(name: string, age: number): User;
function join(name: string, age: string): string;
function join(name: any, age: any): any {
if (typeof age === 'number') {
return { name, age };
} else {
return '나이는 숫자로 입력해 주세요';
}
}
const sma: User = join('Sam', 30);
const jame: string = join('Jane', '30');
const jame2: string = join(123, '30'); // ERROR !!!
만일 오버로딩 함수들의 매개변수와 타입이 다음과 같이 다양할 경우, 선택적 프로퍼티(?) 를 사용해 선언이 가능하다.
// 함수 선언
function printConsole(a: number): void;
function printConsole(a: string): void;
function printConsole(a: number, b: string): void;
function printConsole(a: string, b: number): void;
// 함수 구현
function printConsole(a: any, b?: any): void {
console.log(a, b);
}
// 함수 호출
printConsole('1');
printConsole('1', 2);
// Error - 오버로딩에 (a: string, b: string) => void 라는 함수 타입이 없기 때문이다.
printConsole('1', '2');
만약 함수의 인자 숫자가 같고, 반환 타입이 같다면 굳이 overload를 사용할 필요 없다.
오히려 간단하게 Union Type으로 해주는게 훨씬 보기 좋다.
// 굳이 오버로딩??
function overloadLen(s: string): number;
function overloadLen(arr: any[]): number;
function overloadLen(x: any) {
return x.length;
}
// 깔쌈하게 유니온으로 구현
function simpleLen(x: any[] | string): number {
return x.length;
}
# 참고자료
https://typescript-kr.github.io/pages/functions.html
https://heropy.blog/2020/01/27/typescript/
이재승 타입스크립트 시작하기
제로초 타입스크립트 올인원
쉽게 시작하는 타입스크립트 (길벗, 2023, 캡틴판교 지음)
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.