TypeScript 의 함수 타이핑
이번엔 함수 / 메서드의 타이핑에 대해 알아보자. 타입스크립트에서는 함수를 타이핑하는 방법을 여러 개 제공하고 있어, 한 번 다뤄볼 필요가 있다.
함수 타이핑
매개변수 타이핑
1
2
3
4
5
function example (a: string, b?: number, c = false) {}
example('hi', 123, true);
example('hi', 123);
example('hi');
옵셔널 키워드와, 기본값을 할당하는 방법으로 함수의 매개변수를 타이핑한 예시다.
1
2
3
function example1(a: string, ...b: number[]) {}
example1('hi', 123, 4, 56);
function example2(...a: string[], b: number) {} // A rest parameter must be last in a parameter list.
함수의 매개변수에 나머지(rest) 연산자를 사용했다. example1
에서는 문제가 없지만, example2
에서는 나머지 매개변수는 마지막에 나와야한다면서 오류를 일으킨다.
배열에서 example2
처럼 타이핑을 한다면, 마지막 매개변수만 number 이고 나머지는 모두 string 으로 지정이 될텐데 함수에서는 불가능하다.
매개변수에 구조분해 할당을 사용하는 경우
이 경우에 타이핑을 직접하려면 조금 혼란이 올 수 있는데, 예시를 통해 살펴보자.
1
2
function des({prop: { nested }}) {} // Binding element 'nested' implicitly has an 'any' type.
des({ prop: { nested: 'hi' }});
위 처럼 사용하게되면, 추론이 되지 않아 타이핑이 필요하다.
1
2
function des({ prop: { nested: string }}) {} // Binding element 'string' implicitly has an 'any' type.
des({ prop: { nested: 'hi'}});
그럼 이렇게 타이핑하면 올바르게 된 걸까?
이 역시 any 로 추론이 되는 문제가 발생했다. 게다가 nested 가 아니고 string 이 any 타입이라니! 지금 위 처럼 사용하면, nested 속성을 string 이라는 이름으로 바꾼 것 뿐이다.
자바스크립트에서 이렇게 사용하기 때문에, 아래와 같은 방법으로 타이핑을 해야한다.
1
2
function des({ prop: {nested}}: { prop: { nested: string }}) {}
des({ prop: { nested: 'hi' }});
함수 내부에서 this 를 사용하는 경우
1
2
3
4
5
6
7
8
9
10
11
12
function example1() {
console.log(this) // 'this' implicitly has type 'any' because it does not have a type annotation.
}
function exmaple2(this: Window) {
console.log(this);
}
function example3(this: Document, a: string, b: 'this') {}
example3('hello', 'this'); // The 'this' implicitly has type 'any' because it does not have a type annotation.
example3.call(document, 'hello', 'this');
this 를 거의 사용하지 않지만, 언젠가는 쓸 수도 있고, 이미 사용하고 있는 코드를 마주할 수 있으니 배워두도록 하자.
현재 위 코드를 보면, example1
에서 오류가 발생한다. this 를 명시적으로 표기하지 않으면, any 로 추론하기 때문이다.
this 를 명시적으로 표기하는 방법은, example2
와 example3
를 보면 된다. 물론, Document 객체는 this 일 수는 없기에 example3
에선 오류가 발생하지만 원한다면 call()
이나 bind()
, apply()
등을 사용하면 된다.
메서드의 경우
자바스크립트의 this 에 대해 알고 있다면, 아마 이 경우엔 this 가 무엇을 가리키고 있는지 알텐데, 이땐 명시적으로 타이핑할 필요가 없다. 당연하지만, 바뀌게 될 때는 명시적으로 대상을 지정해줘야한다. 아래 예시를 확인하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Animal = {
age: number;
type: 'dog',
};
const person = {
name: 'zero',
age: 28,
sayName() {
this;
this.name;
},
sayAge(this: Anmial) {
this; // this: Animal
this.type; // dog
}
}
person.sayAge.bind({ age: 3, type: 'dog'});
함수 오버로딩(Overloading)
자바스크립트는 함수의 매개변수에 어떤 타입을 넣을지, 몇 개를 넣을지 정해져있지 않다. 호출하는 사람이 자유롭게 사용할 수 있는데, 이것에서 발생하는 문제들을 막기 위해 타입스크립트를 사용하는 것이다.
타입스크립트에서는 어떤 타입과 값이 들어올지 지정할 수 있고, 그렇게 해야만 한다.
두 문자열 또는 두 숫자를 더하는 add
함수를 예시로 들어서 살펴보자.
1
2
3
4
5
6
7
8
function add(x: string | number, y: string | number): string | number {
return x + y; // Operator '+' cannot be applied to types 'string | number' and 'string | number'.
}
add(1, 2);
add('1', '2');
add(1, '2'); // 원하지 않는데 연산이 됨
add('1', 2); // 원하지 않는데 연산이 됨
위 처럼 지정하면, 우리가 원하는 결과를 얻을 순 없고 오류까지 발생한다. x, y 와 반환값을 string | number 로 지정했기 때문에, 숫자 1과 문자 2 의 연산이 가능하고 그 반대도 연산이 가능해지기 때문이다.
이럴 때 필요한 기법이 오버로딩(Overloading)이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function add(x: number, y: number): number
function add(x: string, y: string): string
function add(x: any, y: any) {
return x + y;
}
add(1, 2); // 3
add('1', '2'); // '12'
add(1, '2'); /* No overload matches this call.
Overload 1 of 2, '(x: number, y: number): number', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'number'.
Overload 2 of 2, '(x: string, y: string): string', gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string'. */
add('1', 2); /* No overload matches this call.
Overload 1 of 2, '(x: number, y: number): number', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'number'.
Overload 2 of 2, '(x: string, y: string): string', gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string'. */
호출할 수 있는 함수의 타입을 미리 여러 개 타이핑해두는 것이다. number 와 number 끼리 연산해 number 를 반환하는 함수, string 과 string 간의 연산 결과로 string 을 반환하는 함수.
이후, 실제 연산을 진행할 함수는 any 로 타이핑해주면 된다. 조금 이상할 수 있지만, any 를 지정하지 않으면 암묵적으로 any 로 추론하며 implicitAny 에러가 발생한다.
이렇게 오버로딩 기법을 사용하면, 명시적으로 타이핑한 조합을 만족해야만 호출이 가능해진다. 위 오류의 내용을 보자면, Overload 1 of 2
와 Overload 2 of 2
에 대한 언급이 있다.
2가지 오버로딩 중 어느 것에도 해당하지 않아 오류를 일으키는 것이다.
다른 객체 지향 프로그래밍 언어에서는 메소드 오버로딩 / 메소드 오버라이딩 두 가지의 기법이 있다.
메소드 오버로딩은 지금 본 것과 비슷하게 메소드를 오버로딩 하는 것이고,
오버라이딩은 상위 클래스(부모 클래스)를 상속받은 하위 클래스(자식 클래스)에서 상위 클래스에 정의된 메소드를 다시 정의하는 것 이다.
오버로딩의 선언 순서
오버로딩 기법을 활용할 땐, 선언하는 순서도 유의해야한다.
1
2
3
4
5
6
7
8
9
10
11
function example(param: string): string;
function example(param: string | null): number;
function example(param: string | null): string | number {
if (param) {
return 'string';
} else {
return 123;
}
}
const result = example('what'); // const result: string
위 코드에서는 result
의 값이 ‘string’ 이 된다. 오버로딩한 함수 중 string 타입을 입력받을 수 있기 때문이다.
그런데, 순서를 바꾼다면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
function example(param: string | null): number;
function example(param: string): string;
function example(param: string | null): string | number {
if (param) {
return 'string';
} else {
return 123;
}
}
const result = example('what'); // const result: number
추론된 결과를 보니, result
가 number 로 추론했다. 실제로 실행하면 오류가 발생하긴 하지만, 어찌됐든 중요한 것은 좁은 타입 부터 작성하고 넓은 타입을 다음에 작성하는 것이다. 위에서 부터 검사를 하기 때문이다.
타입 별칭과 인터페이스를 활용한 오버로딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Add {
(x: number, y: number): number;
(x: string, y: string): string;
}
const add: Add = (x: any, y: any) => x + y;
add(1, 2); // 3
add('1', '2'); // 12
add(1, '2'); // Error
add('1', 2); // Error
type Add1 = (x: number, y: number) => number;
type Add2 = (x: string, y: string) => string;
type Add = Add1 & Add2;
const add: Add = (x: any, y: any) => x + y;
add(1, 2); // 3
add('1', '2'); // 12
add(1, '2'); // Error
add('1', 2); // Error
이렇게도 오버로딩이 가능하니 참고하자.