함수

함수 (Functions)

함수는 어떤 애플리케이션에서든 가장 기본적인 구성 요소예요. 로컬 함수든, 다른 모듈에서 가져온 거든, 클래스의 메서드든 말이죠. 함수 역시 값이므로, 다른 값들과 마찬가지로 TypeScript에는 함수가 어떻게 호출될 수 있는지 설명하는 많은 방법이 있습니다. 이 장에서는 함수를 설명하는 타입을 어떻게 작성하는지 배워볼게요.

출처: TypeScript 핸드북

함수 타입 표현식 (Function Type Expressions)

함수를 설명하는 가장 간단한 방법은 함수 타입 표현식 입니다. 이 타입들은 화살표 함수와 문법적으로 비슷해요.

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

(a: string) => void 문법은 "매개변수가 하나이고 이름이 a, 타입이 string이며 반환 값이 없는 함수"를 뜻합니다. 함수 선언과 마찬가지로, 매개변수 타입을 지정하지 않으면 암시적으로 any가 됩니다.

매개변수 이름은 필수라는 점에 주의하세요. 함수 타입 (string) => void는 "이름이 string이고 타입이 any인 매개변수를 가진 함수"를 뜻합니다!

물론 타입 별칭을 사용해 함수 타입에 이름을 붙일 수도 있어요.

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

호출 시그니처 (Call Signatures)

JavaScript에서 함수는 호출 가능한 것에 더해 프로퍼티를 가질 수 있어요. 하지만 함수 타입 표현식 문법은 프로퍼티 선언을 허용하지 않습니다. 프로퍼티를 가진 호출 가능한 무엇인가를 설명하고 싶다면, 객체 타입 안에 호출 시그니처 를 작성하면 됩니다.

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

function myFunc(someArg: number) {
  return someArg > 3;
}
myFunc.description = "default description";

doSomething(myFunc);

문법이 함수 타입 표현식과는 약간 다르다는 점을 눈여겨보세요. 매개변수 목록과 반환 타입 사이에 =>가 아니라 :를 사용합니다.

생성 시그니처 (Construct Signatures)

JavaScript 함수는 new 연산자로도 호출될 수 있어요. TypeScript는 이런 것들을 보통 새 객체를 만들기 때문에 생성자 라고 부릅니다. 호출 시그니처 앞에 new 키워드를 붙이면 생성 시그니처 를 작성할 수 있어요.

type SomeObject = any;
// ---cut---
type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

JavaScript의 Date 객체 같은 일부 객체는 new를 붙이거나 붙이지 않고도 호출될 수 있어요. 호출 시그니처와 생성 시그니처를 같은 타입 안에 임의로 조합할 수 있습니다.

interface CallOrConstruct {
  (n?: number): string;
  new (s: string): Date;
}

function fn(ctor: CallOrConstruct) {
  // Passing an argument of type `number` to `ctor` matches it against
  // the first definition in the `CallOrConstruct` interface.
  console.log(ctor(10));
              // ^?

  // Similarly, passing an argument of type `string` to `ctor` matches it
  // against the second definition in the `CallOrConstruct` interface.
  console.log(new ctor("10"));
                  // ^?
}

fn(Date);

제네릭 함수 (Generic Functions)

입력의 타입이 출력의 타입과 관련되거나, 두 입력의 타입이 어떤 식으로든 관련된 함수를 작성하는 일은 흔합니다. 배열의 첫 번째 요소를 반환하는 함수를 잠시 생각해 봅시다.

function firstElement(arr: any[]) {
  return arr[0];
}

이 함수는 제 역할을 하지만, 아쉽게도 반환 타입이 any입니다. 함수가 배열 요소의 타입을 반환하면 더 좋겠죠.

TypeScript에서 제네릭 은 두 값 사이의 대응 관계를 설명하고 싶을 때 사용합니다. 함수 시그니처에 타입 매개변수 를 선언해 이 관계를 만들 수 있어요.

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

이 함수에 타입 매개변수 Type을 추가하고 두 곳에서 사용함으로써, 우리는 함수의 입력(배열)과 출력(반환 값) 사이의 연결을 만들었습니다. 이제 호출하면 더 구체적인 타입이 나옵니다.

declare function firstElement<Type>(arr: Type[]): Type | undefined;
// ---cut---
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);

추론 (Inference)

이 예시에서 Type을 명시할 필요가 없었다는 점을 주목하세요. 타입은 TypeScript에 의해 추론 되었습니다 — 자동으로 선택되었죠.

여러 타입 매개변수도 사용할 수 있어요. 예를 들어 독립형 버전의 map은 이렇게 생겼을 거예요.

// prettier-ignore
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}

// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

이 예시에서 TypeScript가 Input 타입 매개변수의 타입(주어진 string 배열로부터)과 Output 타입 매개변수(함수 표현식의 반환 값 number에 기반해)를 둘 다 추론할 수 있었다는 걸 눈여겨보세요.

제약 (Constraints)

우리는 어떤 종류의 값이든 동작할 수 있는 제네릭 함수를 몇 개 작성했습니다. 때로는 두 값을 서로 관계시키되, 특정 부분집합의 값에만 작동하고 싶을 때가 있어요. 그럴 때는 제약 을 사용해 타입 매개변수가 받아들일 수 있는 타입의 종류를 제한할 수 있습니다.

두 값 중 더 긴 것을 반환하는 함수를 작성해 봅시다. 그러려면 숫자인 length 프로퍼티가 필요해요. extends 절을 작성해 타입 매개변수를 그 타입으로 제약 합니다.

// @errors: 2345 2322
function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);

이 예시에서 주목할 만한 흥미로운 점이 몇 가지 있어요. 우리는 TypeScript가 longest의 반환 타입을 추론 하도록 허용했습니다. 반환 타입 추론은 제네릭 함수에서도 동작합니다.

Type{ length: number }로 제약했기 때문에 ab 매개변수의 .length 프로퍼티에 접근할 수 있었어요. 타입 제약이 없었다면, 그 값들이 length 프로퍼티가 없는 다른 타입일 수도 있으므로 그 프로퍼티들에 접근할 수 없었을 거예요.

longerArraylongerString의 타입은 인자에 기반해 추론되었습니다. 기억하세요, 제네릭은 둘 이상의 값을 같은 타입으로 관계 짓는 것에 관한 것입니다!

마지막으로, 우리가 원했던 대로 longest(10, 100) 호출은 number 타입에 .length 프로퍼티가 없어서 거부됩니다.

제약된 값 다루기 (Working with Constrained Values)

제네릭 제약을 다룰 때 흔히 만나는 에러가 하나 있어요.

// @errors: 2322
function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj;
  } else {
    return { length: minimum };
  }
}

이 함수가 괜찮아 보일 수 있어요 — Type{ length: number }로 제약되었고, 함수는 Type 또는 그 제약에 맞는 값을 반환하니까요. 문제는 이 함수가 제약에 맞는 어떤 객체가 아니라, 전달된 것과 같은 종류의 객체를 반환하겠다고 약속한다는 점입니다. 이 코드가 합법이라면, 확실히 동작하지 않을 코드를 작성할 수 있게 됩니다.

declare function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type;
// ---cut---
// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));

타입 인자 지정하기 (Specifying Type Arguments)

TypeScript는 보통 제네릭 호출에서 의도된 타입 인자를 추론할 수 있지만, 항상 그런 건 아니에요. 예를 들어 두 배열을 합치는 함수를 작성했다고 해 봅시다.

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

일반적으로 이 함수를 타입이 안 맞는 배열로 호출하면 에러가 됩니다.

// @errors: 2322
declare function combine<Type>(arr1: Type[], arr2: Type[]): Type[];
// ---cut---
const arr = combine([1, 2, 3], ["hello"]);

이렇게 하려는 의도였다면 Type을 수동으로 지정할 수 있어요.

declare function combine<Type>(arr1: Type[], arr2: Type[]): Type[];
// ---cut---
const arr = combine<string | number>([1, 2, 3], ["hello"]);

좋은 제네릭 함수 작성 가이드라인

제네릭 함수 작성은 재미있어서, 타입 매개변수에 쉽게 과도하게 몰두할 수 있어요. 타입 매개변수가 너무 많거나 필요 없는 곳에 제약을 쓰면 추론이 덜 성공하게 되어 함수 호출자들을 답답하게 만들 수 있습니다.

타입 매개변수를 아래로 밀어내기

비슷해 보이는 두 방식의 함수 작성법이 있습니다.

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}

function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

얼핏 보면 동일해 보이지만, firstElement1이 이 함수를 작성하는 훨씬 좋은 방식입니다. 그 추론된 반환 타입은 Type인데 반해, firstElement2의 추론된 반환 타입은 any입니다. TypeScript가 호출 중에 요소를 "기다렸다가" 해결하는 대신, 제약 타입으로 arr[0] 표현식을 해결해야 하기 때문이죠.

규칙: 가능하면 타입 매개변수를 제약하기보다 타입 매개변수 자체를 사용하세요.

타입 매개변수를 더 적게 쓰기

또 다른 비슷한 함수 쌍이 있어요.

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}

function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

우리는 두 값을 관계 짓지 않는 Func 타입 매개변수를 만들었습니다. 이는 항상 위험 신호인데, 타입 인자를 지정하려는 호출자가 이유 없이 추가 타입 인자를 수동으로 지정해야 한다는 뜻이기 때문이죠. Func는 함수를 읽고 추론하기 더 어렵게 만들 뿐 아무것도 하지 않습니다!

규칙: 항상 가능한 한 적은 타입 매개변수를 사용하세요.

타입 매개변수는 두 번 나타나야 한다

때로는 함수가 제네릭일 필요가 없을 수도 있다는 걸 잊곤 합니다.

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

greet("world");

우리는 더 간단한 버전을 쉽게 작성할 수 있었어요.

function greet(s: string) {
  console.log("Hello, " + s);
}

기억하세요, 타입 매개변수는 여러 값의 타입을 관계 짓기 위한 것입니다. 타입 매개변수가 함수 시그니처에서 한 번만 사용된다면, 관계 짓는 것이 아무것도 없다는 뜻이에요. 여기에는 추론된 반환 타입도 포함됩니다. 예를 들어 Strgreet의 추론된 반환 타입의 일부였다면, 인자와 반환 타입을 관계 짓는 것이므로 작성된 코드에는 한 번만 나타나더라도 두 번 사용된 셈이 됩니다.

규칙: 타입 매개변수가 한 위치에만 나타난다면, 정말 필요한지 강하게 재고하세요.

선택적 매개변수 (Optional Parameters)

JavaScript의 함수는 종종 가변 개수의 인자를 받아요. 예를 들어 numbertoFixed 메서드는 선택적인 자릿수를 받습니다.

function f(n: number) {
  console.log(n.toFixed()); // 0 arguments
  console.log(n.toFixed(3)); // 1 argument
}

TypeScript에서 ?로 매개변수를 선택적 으로 표시해 이를 모델링할 수 있어요.

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

매개변수가 number 타입으로 지정되었지만, 실제로 x 매개변수는 number | undefined 타입을 갖게 됩니다. JavaScript에서 지정되지 않은 매개변수는 undefined 값을 얻기 때문이죠.

매개변수 기본값 을 제공할 수도 있어요.

function f(x = 10) {
  // ...
}

이제 f의 본문에서 xnumber 타입을 갖습니다. 어떤 undefined 인자든 10으로 대체되기 때문이죠. 매개변수가 선택적일 때 호출자는 언제나 undefined를 전달할 수 있다는 점에 주의하세요. 이는 그냥 "누락된" 인자를 시뮬레이션할 뿐입니다.

declare function f(x?: number): void;
// ---cut---
// All OK
f();
f(10);
f(undefined);

콜백에서의 선택적 매개변수

선택적 매개변수와 함수 타입 표현식에 대해 배우고 나면, 콜백을 호출하는 함수를 작성할 때 다음 실수를 하기 쉬워요.

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

사람들이 index?를 선택적 매개변수로 작성하며 보통 의도하는 것은 다음 두 호출 모두가 합법이길 바라는 것입니다.

// @errors: 2532 18048
declare function myForEach(
  arr: any[],
  callback: (arg: any, index?: number) => void
): void;
// ---cut---
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

이것이 실제로 뜻하는 것은 callback이 인자 하나로 호출될 수도 있다 는 것입니다. 다시 말해, 함수 정의는 구현이 이렇게 생길 수도 있음을 말해 줍니다.

// @errors: 2532 18048
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    // I don't feel like providing the index today
    callback(arr[i]);
  }
}

그리고 TypeScript는 이 의미를 강제하며 실제로는 일어날 수 없는 에러를 발행합니다.

// @errors: 2532 18048
declare function myForEach(
  arr: any[],
  callback: (arg: any, index?: number) => void
): void;
// ---cut---
myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});

JavaScript에서 함수를 매개변수보다 많은 인자로 호출하면, 추가 인자들은 그냥 무시됩니다. TypeScript도 동일하게 동작해요. (같은 타입의) 매개변수가 더 적은 함수는 항상 매개변수가 더 많은 함수를 대신할 수 있습니다.

규칙: 콜백용 함수 타입을 작성할 때, 그 인자를 전달하지 않고 함수를 호출 할 의도가 아니라면 절대 선택적 매개변수를 작성하지 마세요.

함수 오버로드 (Function Overloads)

일부 JavaScript 함수는 다양한 인자 개수와 타입으로 호출될 수 있어요. 예를 들어 타임스탬프(인자 하나) 또는 월/일/년 지정(세 인자)을 받는 Date를 만드는 함수를 작성할 수 있습니다.

TypeScript에서 오버로드 시그니처 를 작성해 다양한 방식으로 호출될 수 있는 함수를 지정할 수 있어요. 이를 위해 함수 시그니처 몇 개(보통 둘 이상)를 작성한 다음 함수 본문을 이어서 씁니다.

// @errors: 2575
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

이 예시에서 우리는 두 개의 오버로드를 작성했습니다. 하나는 인자 하나를 받고, 다른 하나는 인자 세 개를 받습니다. 이 첫 두 시그니처를 오버로드 시그니처 라고 부릅니다.

그런 다음 우리는 호환 가능한 시그니처로 함수 구현을 작성했습니다. 함수는 구현 시그니처를 가지지만, 이 시그니처는 직접 호출될 수 없어요. 필수 인자 뒤에 선택적 매개변수 두 개로 함수를 작성했지만, 두 매개변수로는 호출될 수 없는 거죠!

오버로드 시그니처와 구현 시그니처

이것은 흔한 혼란의 원천입니다. 사람들은 다음과 같은 코드를 작성하고 왜 에러가 나는지 이해하지 못하곤 해요.

// @errors: 2554
function fn(x: string): void;
function fn() {
  // ...
}
// Expected to be able to call with zero arguments
fn();

다시 말하지만, 함수 본문을 작성할 때 사용하는 시그니처는 바깥에서 "볼 수" 없습니다.

구현 의 시그니처는 바깥에서 보이지 않습니다. 오버로드된 함수를 작성할 때는 함수의 구현 위에 항상 개 이상의 시그니처를 두어야 합니다.

구현 시그니처는 또한 오버로드 시그니처와 호환 되어야 합니다. 예를 들어 다음 함수들은 구현 시그니처가 오버로드와 올바른 방식으로 일치하지 않아 에러가 있습니다.

// @errors: 2394
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
function fn(x: boolean) {}
// @errors: 2394
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
function fn(x: string | number) {
  return "oops";
}

좋은 오버로드 작성하기

제네릭처럼, 함수 오버로드를 사용할 때 따라야 할 몇 가지 가이드라인이 있어요. 이 원칙들을 따르면 함수를 호출하기 쉽고, 이해하기 쉽고, 구현하기 쉬워집니다.

문자열 또는 배열의 길이를 반환하는 함수를 생각해 보세요.

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}

이 함수는 괜찮아요. 문자열이나 배열로 호출할 수 있으니까요. 하지만 문자열 또는 배열일 수 있는 값으로는 호출할 수 없습니다. TypeScript는 함수 호출을 단일 오버로드로만 해석할 수 있기 때문이죠.

// @errors: 2769
declare function len(s: string): number;
declare function len(arr: any[]): number;
// ---cut---
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);

두 오버로드가 같은 인자 개수와 같은 반환 타입을 갖기 때문에, 대신 오버로드되지 않은 버전의 함수를 작성할 수 있어요.

function len(x: any[] | string) {
  return x.length;
}

이게 훨씬 낫습니다! 호출자는 어떤 종류의 값으로든 이 함수를 호출할 수 있고, 덤으로 올바른 구현 시그니처를 알아내야 할 필요도 없어요.

가능하면 오버로드보다 매개변수에 유니온 타입을 쓰는 것을 항상 선호하세요.

함수에서 this 선언하기

TypeScript는 코드 흐름 분석을 통해 함수에서 this가 무엇이어야 하는지를 추론합니다. 예를 들어 다음에서 말이죠.

const user = {
  id: 123,

  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

TypeScript는 user.becomeAdmin 함수가 바깥 객체 user인 해당 this를 가진다는 것을 이해합니다. this는 많은 경우에 충분하지만, this가 나타내는 객체를 더 제어해야 하는 경우도 많아요. JavaScript 명세는 this라는 이름의 매개변수를 가질 수 없다고 명시하므로, TypeScript는 그 문법 공간을 사용해 함수 본문에서 this의 타입을 선언하게 해 줍니다.

interface User {
  id: number;
  admin: boolean;
}
declare const getDB: () => DB;
// ---cut---
interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

이 패턴은 콜백 스타일의 API에서 흔한데, 보통 다른 객체가 함수를 호출할 시점을 제어하는 경우입니다. 이 동작을 얻으려면 화살표 함수가 아니라 function을 사용해야 한다는 점에 주의하세요.

// @errors: 7041 7017
interface User {
  id: number;
  admin: boolean;
}
declare const getDB: () => DB;
// ---cut---
interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
const admins = db.filterUsers(() => this.admin);

알아두면 좋은 다른 타입들

함수 타입을 다룰 때 자주 등장하는 추가 타입들이 몇 가지 있어요. 모든 타입처럼 어디서든 쓸 수 있지만, 이들은 특히 함수의 맥락에서 관련이 깊습니다.

void

void는 값을 반환하지 않는 함수의 반환 값을 나타냅니다. 함수에 return 문이 없거나, 그 return 문에서 명시적인 값을 반환하지 않을 때마다 추론되는 타입이에요.

// The inferred return type is void
function noop() {
  return;
}

JavaScript에서 값을 반환하지 않는 함수는 암시적으로 undefined 값을 반환합니다. 하지만 TypeScript에서 voidundefined는 같은 것이 아니에요. 이와 관련된 자세한 내용은 이 장의 끝부분에 있습니다.

voidundefined와 같지 않습니다.

object

특별한 타입 object는 원시 값(string, number, bigint, boolean, symbol, null, undefined)이 아닌 모든 값을 가리킵니다. 이는 빈 객체 타입 { }와도 다르고, 전역 타입 Object와도 다릅니다. 아마 여러분은 Object를 절대 쓰지 않을 거예요.

objectObject가 아닙니다. 항상 object를 사용하세요!

JavaScript에서 함수 값은 객체라는 점에 주의하세요. 프로퍼티를 갖고, 프로토타입 체인에 Object.prototype이 있고, Objectinstanceof이며, Object.keys를 호출할 수 있는 등이죠. 이런 이유로 함수 타입은 TypeScript에서 object로 간주됩니다.

unknown

unknown 타입은 어떤 값이든 나타냅니다. 이는 any 타입과 비슷하지만, unknown 값으로는 아무것도 하는 것이 합법적이지 않다는 점에서 더 안전해요.

// @errors: 2571 18046
function f1(a: any) {
  a.b(); // OK
}
function f2(a: unknown) {
  a.b();
}

함수 타입을 설명할 때 유용한 이유는, 함수 본문에 any 값을 가지지 않으면서도 어떤 값이든 받는 함수를 설명할 수 있기 때문이에요.

반대로, 알 수 없는 타입의 값을 반환하는 함수를 설명할 수도 있습니다.

declare const someRandomString: string;
// ---cut---
function safeParse(s: string): unknown {
  return JSON.parse(s);
}

// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);

never

일부 함수는 값을 절대로 반환하지 않아요.

function fail(msg: string): never {
  throw new Error(msg);
}

never 타입은 결코 관찰되지 않는 값을 나타냅니다. 반환 타입에서 이는 함수가 예외를 던지거나 프로그램의 실행을 종료한다는 뜻입니다.

never는 TypeScript가 유니온에 남은 것이 없다고 판단할 때도 나타납니다.

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!
  }
}

Function

전역 타입 Function은 JavaScript의 모든 함수 값에 존재하는 bind, call, apply 같은 프로퍼티를 설명합니다. 또한 Function 타입의 값은 항상 호출될 수 있다는 특별한 성질이 있고, 그 호출은 any를 반환합니다.

function doSomething(f: Function) {
  return f(1, 2, 3);
}

이는 타입이 없는 함수 호출 이며, 안전하지 않은 any 반환 타입 때문에 일반적으로 피하는 게 좋습니다. 임의의 함수를 받아들여야 하지만 호출할 의도가 없다면, () => void 타입이 일반적으로 더 안전합니다.

나머지 매개변수와 인자 (Rest Parameters and Arguments)

나머지 매개변수 (Rest Parameters)

선택적 매개변수나 오버로드를 사용해 고정된 다양한 인자 개수를 받는 함수를 만드는 것 외에도, 나머지 매개변수 를 사용해 무제한 개수의 인자를 받는 함수를 정의할 수 있습니다.

나머지 매개변수는 다른 모든 매개변수 뒤에 나타나며 ... 문법을 사용합니다.

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

TypeScript에서 이 매개변수들의 타입 주석은 암시적으로 any 대신 any[]이며, 주어진 타입 주석은 Array<T> 또는 T[] 형식이거나 튜플 타입(뒤에서 배울)이어야 합니다.

나머지 인자 (Rest Arguments)

반대로, 이터러블 객체(예: 배열)에서 스프레드 문법을 사용하여 가변 개수의 인자를 제공할 수 있습니다. 예를 들어 배열의 push 메서드는 몇 개의 인자든 받습니다.

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

일반적으로 TypeScript는 배열이 불변이라고 가정하지 않는다는 점에 주의하세요. 이는 다소 놀라운 동작을 일으킬 수 있어요.

// @errors: 2556
// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);

이 상황에 대한 최선의 해결책은 코드에 따라 다르지만, 일반적으로 const 문맥이 가장 간단한 해결책입니다.

// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

나머지 인자를 사용할 때는 오래된 런타임을 대상으로 할 경우 downlevelIteration을 켜야 할 수도 있어요.

매개변수 구조 분해 (Parameter Destructuring)

매개변수 구조 분해를 사용하면 인자로 제공된 객체를 함수 본문의 하나 이상의 지역 변수로 편리하게 풀어낼 수 있어요. JavaScript에서는 이렇게 생겼습니다.

function sum({ a, b, c }) {
  console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });

객체의 타입 주석은 구조 분해 문법 뒤에 옵니다.

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

조금 장황해 보일 수 있지만, 여기에 이름 있는 타입을 사용할 수도 있어요.

// Same as prior example
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}

함수의 할당 가능성 (Assignability of Functions)

반환 타입 void

함수의 void 반환 타입은 다소 특이하지만 예상된 동작을 만들어낼 수 있어요.

void 반환 타입을 가진 상황적 타이핑은 함수가 무엇을 반환하지 않도록 강제하지는 않습니다. 다시 말하면, void 반환 타입의 맥락적 함수 타입(type voidFunc = () => void)은 구현 시 다른 어떤 값이든 반환할 수 있지만, 그 값은 무시될 거예요.

따라서 () => void 타입의 다음 구현들은 모두 유효합니다.

type voidFunc = () => void;

const f1: voidFunc = () => {
  return true;
};

const f2: voidFunc = () => true;

const f3: voidFunc = function () {
  return true;
};

그리고 이 함수들 중 하나의 반환 값이 다른 변수에 할당될 때, 그것은 void 타입을 유지합니다.

type voidFunc = () => void;

const f1: voidFunc = () => {
  return true;
};

const f2: voidFunc = () => true;

const f3: voidFunc = function () {
  return true;
};
// ---cut---
const v1 = f1();

const v2 = f2();

const v3 = f3();

이 동작이 존재하는 이유는, Array.prototype.push가 숫자를 반환하고 Array.prototype.forEach 메서드가 void 반환 타입의 함수를 기대함에도 다음 코드가 유효하도록 하기 위해서입니다.

const src = [1, 2, 3];
const dst = [0];

src.forEach((el) => dst.push(el));

주의해야 할 또 하나의 특수한 경우가 있습니다. 리터럴 함수 정의가 void 반환 타입을 가질 때, 그 함수는 아무것도 반환해서는 안 됩니다.

function f2(): void {
  // @ts-expect-error
  return true;
}

const f3 = function (): void {
  // @ts-expect-error
  return true;
};

void에 대해 더 자세한 내용은 다음 문서 항목들을 참고하세요.

더 알아보기 (Learn more)