TypeScript 핸드북 5 - 함수

함수 (Function)

소개

함수는 JavaScript의 응용 프로그램을 구성하는 기본 요소입니다. 그리고 함수는 추상화 계층, 클래스, 정보 숨기기, 모듈을 모방하는 방법입니다. TypeScript에서는 클래스, 네임 스페이스 및 모듈이 있지만 함수는 여전히 작업 수행 방법을 설명하는 데 중요한 역할을합니다. 또한 TypeScript는 표준 JavaScript 함수에 몇 가지 새로운 기능을 추가하여 보다 쉽게 작업할 수 있도록합니다.

함수

JavaScript와 마찬가지로, 명명 함수(Named function) 또는 익명 함수(Anonymous function)로 TypeScript 함수를 생성할 수 있습니다. 이를 통해 애플리케이션에 가장 적합한 방법을 선택할 수 있으며, API에서 기능 목록을 작성하든, 다른 기능으로 이전할 수있는 일회성 기능을 사용하든 상관 없습니다.

JavaScript에서 이러한 두 가지 접근 방식은 아래의 예제를 보면 쉽게 이해할 수 있습니다.

1
2
3
4
5
6
7
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x+y; };

JavaScript와 마찬가지로 TypeScript 함수는 함수 본문 외부의 변수를 참조할 수 있습니다. 이런 행위를 보통 변수를 Capture 한다고 합니다. 이것이 어떻게 작동하는지 이해하는 것과, 사용에 대한 trade-off는 이 문서의 범위를 벗어납니다. 하지만 이러한 메커니즘이 JavaScript와 TypeScript에서 아주 중요한 요소라는 것을 이해하고 있어야 합니다.

1
2
3
4
5
let z = 100;
function addToZ(x, y) {
return x + y + z;
}

함수 타입

함수 작성하기

우선 이전의 간단한 예제에 타입을 추가해 보겠습니다.

1
2
3
4
5
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };

각 파라미터에 타입을 추가 하고, 리턴 타입을 추가하여 함수에 타입을 부여할 수 있습니다. TypeScript는 return 문을 보고 리턴 타입을 파악할 수 있으므로 많은 경우에 선택적으로 리턴 타입을 생략할 수도 있습니다.

함수 타입 작성

함수를 작성 했으니 함수의 각 타입을 살펴보고 함수의 전체 타입을 작성해 보겠습니다.

1
2
let myAdd: (x: number, y: number)=>number =
function(x: number, y: number): number { return x+y; };

위 코드의 함수 형식은 파라미터의 타입과 리턴 타입의 두 부분이 동일합니다. 전체 함수 타입을 작성할 때 두 부분이 필요합니다. 우리는 파라미터 목록과 마찬가지로 파라미터 타입을 작성하여 각 파라미터에 이름과 타입을 제공합니다. 이름은 가독성을 돕기위한 것입니다. 다음과 같이 작성할 수 있습니다.

1
2
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };

파라미터 타입이 있으면 함수 타입에 파라미터를 지정하는 이름에 상관없이 함수의 유효한 타입으로 간주됩니다.

두 번째 부분은 리턴 타입입니다. 파라미터와 리턴 타입 사이에 굵은 화살표 (=>)를 사용하여 리턴 타입을 명확하게 합니다. 앞에서 언급했듯이, 이것은 함수 타입의 필수 부분이므로, 함수가 값을 반환하지 않는다면 그 값을 그대로 두는 대신에 void를 사용할 수 있습니다.

파라미터와 리턴 타입만 함수 타입을 구성합니다. Capture된 변수는 타입에 반영되지 않습니다. 실제로 Capture된 변수는 함수의 “숨겨진 상태”의 일부이며 해당 API를 구성하지 않습니다.

타입 추정

아래의 예제에서 한쪽에 타입있고 다른쪽에 타입이 없는 경우 TypeScript 컴파일러에서 타입을 알아낼 수 있습니다.

1
2
3
4
5
6
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters 'x' and 'y' have the type number
let myAdd: (baseValue:number, increment:number) => number =
function(x, y) { return x + y; };

이를 타입추론의 한 형태 인 “컨텍스트 타입 지정”이라고 합니다. 이렇게하면 프로그램 입력을 위한 노력이 줄어들수 있습니다.

Optional 과 Default 파라미터

TypeScript에서는 모든 파라미터가 함수에 필요하다고 가정합니다. 이것은 여러분이 null 또는 undefined를 제공할 수 없다는 것을 의미하는 것이 아니라, 컴파일러는 함수가 호출 될 때 사용자가 각 파라미터에 값을 입력했는지 확인한다는 의미입니다. 컴파일러는 또한 이 파라미터들이 함수에만 전달되는 파라미터라고 가정합니다. 즉, 함수에 주어진 파라미터의 수는 함수가 예상하는 파라미터의 수와 일치해야합니다.

1
2
3
4
5
6
7
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, 파라미터 수가 너무 적습니다.
let result2 = buildName("Bob", "Adams", "Sr."); // error, 파라미터 수가 너무 많습니다.
let result3 = buildName("Bob", "Adams"); // 맞습니다.

JavaScript에서 모든 파라미터는 선택 사항이며 사용자가 적합하다고 생각되는 파라미터를 그대로 둘 수 있습니다. 그렇게할때 그 파라미터는 undefined입니다. TypeScript에서 이러한 Optional 파라미터는 파라미터 끝에 ?를 추가하면 가능합니다. 예를 들어 위의 lastName 파라미터를 Optional로 사용 하고자 한다고 가정해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // 올바르게 실행 됩니다.
let result2 = buildName("Bob", "Adams", "Sr."); // error, 파라미터 수가 너무 많습니다.
let result3 = buildName("Bob", "Adams"); // 맞습니다.

모든 Optional 파라미터는 Required 파라미터 다음에 나와야 합니다. lastName보다는 firstNameOptional 파라미터로 변경하려고 한다면 함수에서 파라미터의 순서를 변경해야 하며, 목록의 firstName을 마지막으로 이동해야 합니다.

TypeScript에서 사용자가 파라미터를 입력하지 않거나 파라미터 값 대신 undefined를 전달해도 파라미터가 할당 될 값을 Default 값으로 설정할 수 있습니다. 이를 Default-initialized 파라미터라고 합니다. 그렇기 때문에 아래 코드와 같이 lastNameDefault 값을 "Smith"로 설정할 수 있습니다.

1
2
3
4
5
6
7
8
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right

Required 파라미터 다음에 오는 Default-initialized 파라미터는 Optional 파라미터처럼 취급되며 Optional 파라미터처럼 해당 함수를 호출할 때 생략할 수 있습니다. 이것이 Default-initialized 파라미터가 Optional 파라미터와 공유하는 특성입니다.

1
2
3
function buildName(firstName: string, lastName?: string) {
// ...
}

그리고

1
2
3
function buildName(firstName: string, lastName = "Smith") {
// ...
}

(firstName: string, lastName?: string) => string 같은 타입을 공유합니다. lastNameDefault 값은 파라미터가 Optional 이라는 사실만 남겨두고 타입이 사라집니다.

일반 Optional 파라미터와 달리 Default-initialized 파라미터는 Required 파라미터 다음에 올 필요가 없습니다. Default-initialized 파라미터가 Required 파라미터 앞에 오는 경우 사용자는 명시적으로 undefined를 전달하여 Default-initialized된 값을 가져 오도록 해야합니다. 예를 들어, 우리는 firstName의 기본 초기화(Default initializer)만 사용하여 마지막 예제를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"

나머지(Rest) 파라미터

Required 파라미터, Optional 파라미터 및 Default-initialized 파라미터는 모두 한 가지 공통점이 있습니다. 즉, 한 번에 하나의 파라미터에 대한 지정입니다. 때로는 여러 파라미터를 그룹으로 사용하거나 함수가 궁극적으로 취할 파라미터의 수를 모를 수 있습니다. JavaScript에서는 모든 함수 본문에서 볼 수있는 arguments 변수를 사용하여 파라미터를 직접 사용할 수 있습니다.

TypeScript에서는 이러한 파라미터들을 모아서 함께 변수로 대입할수 있습니다.

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

Rest 파라미터는 무한한 개수의 Optional 파라미터로 취급됩니다. Rest 파라미터에 인수를 전달할 때 원하는 만큼의 파라미터를 입력할수 있습니다. 그리고 또한 아무것도 입력하지 않아도 됩니다. 컴파일러는 줄임표 (...)로 입력된 이름에 나머지 파라미터들을 배열로 만들어 입력합니다. 이 배열 파라미터는 함수 내부에서 사용할 수 있습니다.

줄임표(...) 부호는 Rest 파라미터가있는 함수의 타입에도 사용됩니다.

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

JavaScript에서 this를 사용하는 법을 배우는 것은 기본 통과 의례중 하나입니다.

TypeScript는 JavaScript의 상위 집합이기 때문에 TypeScript 개발자는 this를 사용하는 방법과, 올바르게 사용되지 않는 지점을 찾는 방법을 배워야합니다. 다행히 TypeScript를 사용하면 두 가지 기술을 사용하여 잘못된 용도를 잡을 수 있습니다. this가 JavaScript에서 어떻게 작동하는지 알아야 할 필요가 있다면, Yehuda Katz의 JavaScript 함수 호출및 “this”의 이해를 먼저 읽으십시오. Yehuda의 포스트는 this의 내부 동작을 잘 설명하므로 여기서는 기본 내용만 다룰 것입니다.

this와 화살표 함수(Arrow function)

JavaScript에서 this는 함수가 호출 될 때 설정된 변수입니다. 이것은 매우 강력하고 유연한 기능이지만 함수가 실행될 때 항상 알 필요는 없습니다. 이것은 특히 함수를 반환하거나 함수를 인수로 전달할 때 혼란스러울 수 있습니다.

예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

createCardPicker는 그 자체로 함수를 반환하는 함수입니다. 예제를 실행하려고 하면 예상되는 경고 상자 대신 오류가 발생합니다. 이것은createCardPicker에 의해 생성된 함수에서 사용되는 thisdeck 객체 대신에 window로 설정되기 때문입니다. 왜냐하면 우리는cardPicker() 자신을 호출하기 때문입니다. 이와 같은 최상위 비 메소드 호출 구문은 thiswindow를 사용합니다. (주의 : 엄격 모드에서 thiswindow보다는undefined가 될 것입니다).

사용할 함수를 반환하기 전에 함수가 올바른 this에 바인딩되어 있는지 확인하면 이 문제를 해결할 수 있습니다. 이렇게하면 나중에 함수가 사용되는 방법에 관계없이 원래의 deck 객체를 바라보게 됩니다. 이를 위해 함수 표현식을 변경하여 ECMAScript 6 Arrow 함수 구문을 사용합니다. Arrow 함수는 호출된 곳이 아닌 함수가 생성된 곳의 this를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

더 좋은 점은 컴파일러에 --noImplicitThis 플래그를 넘겨 주면 실수 했을 때 경고를 합니다. 이것은 this.suits[pickedSuit]thisany 타입이라는 것을 나타냅니다.

this 파라미터

불행히도,this.suits[pickedSuit]의 타입은 여전히 any입니다. 왜냐하면 this는 객체 리터럴 내부의 함수 표현식에서 나온 것이기 때문입니다. 이 문제를 해결하기 위해 명시 적으로 this 파라미터를 제공할 수 있습니다. this 파라미터는 함수의 파라미터 목록에서 처음 나오는 가짜 파라미터입니다.

1
2
3
function f(this: void) {
// 이 Standalone 함수에서 'this'를 사용할 수 없는지 확인하십시오.
}

위의 예제 인 CardDeck에 몇 가지 인터페이스를 추가하여 타입을 더 명확하고 쉽게 재사용할 수있게 해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

이제 TypeScript는 createCardPickerDeck 객체에서 호출 될 것으로 예상합니다. 즉,thisany가 아닌 Deck 타입입니다. 따라서 --noImplicitThis는 에러를 발생시키지 않습니다.

콜백에서의 this 파라미터

나중에 호출 하는 함수를 라이브러리에 전달할 때 콜백에서 this를 사용하면 오류가 발생할 수도 있습니다. 콜백을 호출하는 라이브러리가 정상 함수처럼 호출하기 때문에 thisundefined가 됩니다. 어떤 작업으로 콜백 에러를 막기 위해 this 파라미터를 사용할 수 있습니다. 먼저 라이브러리 작성자가 콜백 유형에 this를 사용하여 annotate를 추가해야합니다.

1
2
3
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: voidaddClickListeneronclickthis 타입을 필요로하지 않는 함수라는 것을 의미합니다. 둘째, this를 사용하여 호출 코드에 annotate를 달아야 합니다.

1
2
3
4
5
6
7
8
9
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

this가 annotate 처리되어 있으면, onClickBad는 반드시 Handler의 인스턴스에서 호출되어야 한다는 것을 기술해야 합니다. 그러면 TypeScript는 addClickListenerthis: void가 있는 함수가 필요하다는 것을 탐지합니다. 오류를 수정하려면 this의 타입을 아래와 같이 변경해야 합니다.

1
2
3
4
5
6
7
8
9
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use this here because it's of type void!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

onClickGoodthis 타입을 void로 지정했기 때문에 addClickListener로 넘겨 줄 수 있습니다. 그것은 thisthis.info를 사용할 수 없다는 것을 의미합니다. 둘 다 원한다면 Arrow 함수를 사용해야합니다.

1
2
3
4
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}

이것은 Arrow 함수는 thisCapture하지 않기 때문에 효과적입니다. 그래서 항상 this: void를 기대하는 것으로 넘겨 줄 수 있습니다. 단점은 타입 핸들러의 객체별로 하나의 Arrow 함수가 생성된다는 것입니다. 메소드는 한번 작성되어 Handler의 프로토 타입에 Attach 됩니다. 그리고 핸들러 타입의 모든 객체에 공유됩니다.

Overloads

JavaScript는 본질적으로 매우 동적인 언어입니다. 단일 JavaScript 함수가 전달된 파라미터의 모양을 기반으로 여러 타입의 객체를 반환하는 경우는 일반적인 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

여기서 pickCard 함수는 사용자가 무엇을 전달했는지에 따라 두 가지 다른 것을 반환합니다. 사용자가 Deck를 나타내는 객체를 전달하면 함수가 Card를 선택합니다. 사용자가 Card를 선택하면 우리는 그들이 선택한 Card를 알려줍니다. 이것을 타입 시스템에 어떻게 설명해야 할까요?

대답은 Overload 목록과 동일한 함수에 대해 여러 함수 타입을 제공하는 것입니다. 이 목록은 컴파일러가 함수 호출을 해결하는 데 사용할 것입니다. pickCard가 받아들이는 것과 그것이 반환하는 것을 설명하는 Overload 리스트를 생성해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

위와 같은 변경으로 Overload는 이제 pickCard 함수에 대한 타입이 체크된 호출을 제공합니다.

컴파일러가 올바른 타입 체크를 선택하도록 하기 위해 기본 JavaScript와 비슷한 과정을 거칩니다. Overload 목록을 보고 제공된 파라미터로 함수를 호출하는 첫 번째 Overload 시도를 계속합니다. 일치하는 항목이 있으면 이 Overload를 올바른 Overload로 선택합니다. 이런 이유 때문에 Overload 요청에 따라 가장 구체적인 것에서 덜 구체적인 것 순으로 넘어갑니다.

function pickCard(x): any는 오버로드 리스트의 일부가 아니므로 객체가 있는 Overload, 숫자가 있는 Overload 두 가지 Overload만 있습니다. pickCard를 다른 파라미터 유형과 함께 호출하면 오류가 발생합니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/functions.html]

참고

공유하기