오버로드
- C나 Java같은 정적 타입 언어의 경우, 매개변수의 갯수와 타입에 따라서 정적으로 여러 개의 함수를 구현할 수 있습니다. 하지만 JavaScript는 하나의 함수 내부에서
타입 검사를 해서 동적으로 오버로딩을 구현할 수 있습니다.
- TypeScript의 경우, 오버로딩이 불가능하지는 않지만 그 방식은 정적 타입 언어인 C나 Java보다는 동적 타입언어인 JavaScript와 유사합니다. 다른 인자를 받는 여러 개의 함수를 구현하는 것이 아니라,
하나의 함수에서 인자의 타입이나 갯수에 따라 여러 분기를 태우는 것입니다.
- 일단 TypeScript는 함수에 정해진 타입과 정해진 수의 인자를 넘기지 않으면 에러를 발생시키므로 함수를 선언할 때 어느정도의 유연성을 확보해야 오버로딩을 구현할 수 있습니다. 인자 갯수의 유연성을 확보하는 것이 바로
Optional Parameter 입니다.
하고 싶은 오버로드(비지터 패턴)
public class ComputerPartDisplayVisitor implements ComputerPartVisitor {
public void visit(Computer computer) {
System.out.println("Displaying Computer.");
}
public void visit(Mouse mouse) {
System.out.println("Displaying Mouse.");
}
public void visit(Keyboard keyboard) {
System.out.println("Displaying Keyboard.");
}
public void visit(Monitor monitor) {
System.out.println("Displaying Monitor.");
}
}
- 자바스크립트에서 위와 같이 구현하기 위해서 할 수 있는 방법
class ComputerPartDisplayVisitor implements ComputerPartVisitor {
visit(target: ComputerPart) {
if (target instanceof Computer) {
console.log('Displaying Computer.');
}
if (target instanceof Mouse) {
console.log('Displaying Mouse.');
}
if (target instanceof Keyboard) {
console.log('Displaying Keyboard.');
}
if (target instanceof Monitor) {
console.log('Displaying Monitor.');
}
}
타입스크립트에서의 오버로드
CASE 1 : 함수 선언식
function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: any, b: any) {
return a + b;
}
add('2', '3');
add(2, 3);
add('2', 3);
function add(a, b) {
return a + b;
}
declare function add(a: string, b: string): string;
declare function add(a: number, b: number): number;
CASE 2 : 함수 표현식
type SAdd = (a: string, b: string) => string;
type NAdd = (a: number, b: number) => number;
const add: SAdd & NAdd = function (a: any, b: any) {
return a + b;
};
add('2', '3');
add(2, 3);
add('2', 3);
const add = function (a, b) {
return a + b;
};
declare type SAdd = (a: string, b: string) => string;
declare type NAdd = (a: number, b: number) => number;
declare const add: SAdd & NAdd;
CASE 3 : 인터섹션 안 쓰기
type Add = {
(a: string, b: string): string;
(a: number, b: number): number;
};
const add: Add = function (a: any, b: any) {
return a + b;
};
add('2', '3');
add(2, 3);
add('2', 3);
const add = function (a, b) {
return a + b;
};
declare type Add = {
(a: string, b: string): string;
(a: number, b: number): number;
};
declare const add: Add;
CASE 4 : 타입 바로 선언하기
const add: {
(a: string, b: string): string;
(a: number, b: number): number;
} = function (a: any, b: any) {
return a + b;
};
add('2', '3');
add(2, 3);
add('2', 3);
const add = function (a, b) {
return a + b;
};
declare const add: {
(a: string, b: string): string;
(a: number, b: number): number;
};
함수 선언식 vs 함수 표현식
함수 선언식 - Function Declarations
- 일반적인 프로그래밍 언어에서의 함수 선언과 비슷한 형식입니다.
function 함수명() {
}
함수 표현식 - Function Expressions
- 유연한 자바스크립트 언어의 특징을 활용한 선언 방식입니다.
var 함수명 = function () {
};
함수 선언식과 표현식의 차이점
호이스팅
- 함수 선언식은
호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않습니다.
- 함수 선언식은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어 올려집니다.
- 함수 표현식에서 변수 선언도 호이스팅이 적용되어 위치가 상단으로 끌어올려집니다. 하지만 function 로직은 호출된 이후에 선언됩니다.
함수 표현식의 장점
- 클로저로 사용
- 콜백으로 사용(다른 함수의 인자로 넘길 수 있습니다)
이펙티브 타입스크립트 아이템 12 : 함수 표현식에 타입 적용하기
- 타입스크립트에서는
함수 표현식을 사용하는 것이 좋습니다. 함수의 매개변수 부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있습니다.
- 매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 코드도 간결하고 안전합니다.
- 만약 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해 내거나 이미 존재하는 타입을 찾아보도록 합니다. 라이브러리를 직접 만든다면 공통 콜백에 타입을 제공해야 합니다.
- 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때는 매개변수의 타입과 반환 타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용해야 합니다.
- 다른 함수의 시그니처를 참조하려면 typeof fn을 사용하면 됩니다.
function add(a: number, b: number): number {
return a + b;
}
function sub(a: number, b: number): number {
return a - b;
}
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
참고 의견
- 표현식, 함수가 나뉘는 이유는 JS this 바인딩 때문일것이고, 함수 오버로딩은 JS 스펙 상 불가능하니, 래퍼함수로써 사용할 것을 유도하는 것이 아닐까 싶습니다. JS의 수퍼셋으로써 동작을 해야하니, 윤희님이 원하시는 일반적인 오버로딩 구현은 불가하고, JS 세상에선 오버로딩 == 타입 래핑함수 거치기 라는 국룰이 있슴당.
- 표현식, 선언식과 this는 무관합니다. function과 arrow function의 차이입니다.
- 인터페이스건 객체 타입이건, 정말로 양변하는 타입을 의도한 것이 아니면 항상 화살표 표기법을 사용하는 것이 좋다는 내용이 있기는 합니다.
- 인터섹션(&) 개념이 어렵고 D.TS의 정의가 길어집니다.
- D.TS의 정의가 길어지는 것은 타입스크립트 컴파일러를 믿고 가야하지 않을까 싶습니다...ㅠ
- 인터섹션을 사용하지 않고 표현식으로 사용하는 방법을 고민해 보았습니다.
결론
- 오버로드가 단순히 input, output에 대한 interface 정의만 한다면 타입으로 정의하는 것이 더 좋지 않을까라고 생각을 했으나 오버로드를 위한 선언과 구현을 올바르게 한 것인가에 대해서는 그렇지 않습니다.
- type으로 선언한 것도 선언한 것의 이름을 대변하는 이름을 붙인 것이고 구현 했다고 생각하는 것도 정의 된 것을 구현한 것이 아닌 익명함수를 넣고 그것과 타입이 같은 것일 뿐입니다.
- 연관된 값들이라면 엮여 있고 찾기 쉽게 끔 작성하는 것이 더 좋지 않을까라고 생각했지만 그것의 역할은 IDE의 역할입니다.
- 실제 작성할 때 힌트를 노출하는 것과 함수를 눌러서 구현을 찾아가는 것은 IDE가 지원해 주기 때문에 크게 걱정할 요소가 아닙니다.
- 일반적으로 널리 사용되는 방식을 사용하는 것이 좋습니다.
- lib.dom.d.ts와 같은 일반 라이브러리나 많은 예시들에서 CASE 1을 사용합니다.
- 컴파일러가 알맞은 타입 검사를 하기 위해서, JavaScript와 비슷한 프로세스를 따릅니다. 오버로드 목록에서 첫 번째 오버로드를 진행하면서 제공된 매개변수를 사용하여 함수를 호출하려고 시도합니다. 만약 일치하게 된다면 해당 오버로드를 알맞은 오버로드로 선택하여 작업을 수행합니다. 이러한 이유로 가장 구체적인 것부터 오버로드 리스팅을 하는 것이 일반적입니다.
- 함수 표현식을 사용하는 것이 좋은 경우도 있지만 무조건적으로 사용하기 보다 상황에 맞게 사용해야 하는데 함수 오버로드의 개념으로 볼때는 표현식을 사용하는 것이 올바르지 않습니다.
추가적인 코드 개선
문제점
function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: any, b: any) {
return a + b;
}
add('2', '3');
add(2, 3);
function f(x: string | number) {
add(x, x);
}
아이템 50 : 오버로딩 타입보다는 조건부 타입을 사용하기
function add<T extends string | number>(
a: T,
b: T,
): T extends string ? string : number;
function add(a: any, b: any) {
return a + b;
}
add('2', '3');
add(2, 3);
function f(x: string | number) {
add(x, x);
}
add('2', 3);
참고