함수

함수

함수는 모든 애플리케이션의 기본 구성 요소예요. 로컬 함수든, 다른 모듈에서 불러온 함수든, 클래스의 메서드든 결국은 함수죠. 그런데 함수를 단순한 코드 조각으로만 보면 안 돼요. 함수도 값이거든요. 그래서 TypeScript에서는 "이 함수를 어떻게 호출할 수 있는지"를 타입으로 표현하는 방법이 여럿 있어요. 이번 장에서는 함수를 설명하는 타입을 어떻게 쓰는지 하나씩 살펴볼게요.

출처: TypeScript 공식문서

본문

함수 타입 표현 (Function Type Expressions)

함수를 설명하는 가장 간단한 방법은 함수 타입 표현식(function type expression)을 쓰는 거예요. 이 타입은 화살표 함수와 문법이 아주 비슷해요:

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라는 함수 타입은 "타입이 anystring이라는 이름의 매개변수를 받는 함수"라는 뜻이 돼버리거든요!

물론 타입 별칭을 써서 함수 타입에 이름을 붙일 수도 있어요:

type GreetFunction = (a: string) => void;

호출 시그니처 (Call Signatures)

자바스크립트에서는 함수가 호출 가능한 대상이면서 동시에 프로퍼티를 가질 수 있어요. 그런데 함수 타입 표현식 문법으로는 프로퍼티를 선언할 수 없어요. 호출 가능하면서 프로퍼티까지 있는 대상을 설명하고 싶다면, 객체 타입 안에 호출 시그니처(call signature)를 쓰면 돼요:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};

function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

문법이 함수 타입 표현식과 약간 달라요. 매개변수 목록과 반환 타입 사이에 =>가 아니라 :를 쓴다는 점만 기억하면 돼요.

생성 시그니처 (Construct Signatures)

자바스크립트 함수는 new 연산자로 호출될 수도 있어요. TypeScript에서는 이들을 생성자(constructor)라고 부르는데, 보통 새 객체를 만들기 때문이에요. 생성 시그니처(construct signature)는 호출 시그니처 앞에 new 키워드를 붙여서 써요:

type SomeConstructor = {
  new (s: string): SomeObject;
};

function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Date 객체처럼 new를 붙여도 되고 안 붙여도 되는 객체도 있어요. 호출 시그니처와 생성 시그니처는 같은 타입 안에서 자유롭게 조합할 수 있어요:

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

제네릭 함수 (Generic Functions)

입력 타입과 출력 타입이 서로 관련 있거나, 두 입력의 타입이 어떤 식으로든 연결된 함수를 작성하는 일은 흔해요. 배열의 첫 번째 요소를 반환하는 함수를 잠깐 생각해볼게요:

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

이 함수는 제 일은 하지만, 아쉽게도 반환 타입이 any예요. 배열 요소의 타입을 그대로 돌려주는 게 훨씬 낫겠죠.

TypeScript에서 제네릭(Generics)은 두 값 사이의 대응 관계를 표현하고 싶을 때 사용해요. 함수 시그니처에 타입 매개변수(type parameter)를 선언하는 방식으로요:

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

타입 매개변수 Type을 함수에 추가하고 두 군데에서 사용하니, 함수의 입력(배열)과 출력(반환값) 사이에 연결고리가 생겼어요. 이제 호출하면 더 구체적인 타입이 나와요:

// 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을 만들면 이런 모양이 되죠:

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)

지금까지는 어떤 종류의 값이든 다룰 수 있는 제네릭 함수를 작성했어요. 그런데 두 값을 연결하되, 특정 부분집합의 값에만 연산을 적용하고 싶을 때도 있죠. 그럴 때는 제약(constraint)으로 타입 매개변수가 받을 수 있는 타입의 종류를 제한할 수 있어요.

두 값 중 더 긴 쪽을 반환하는 함수를 써볼게요. 그러려면 숫자인 length 프로퍼티가 필요해요. extends 절을 써서 타입 매개변수를 그 타입으로 제한합니다:

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);

이 예제에서 재미있는 점이 몇 가지 있어요. longest의 반환 타입은 TypeScript가 추론하도록 놔뒀는데, 반환 타입 추론은 제네릭 함수에서도 잘 동작해요. Type{ length: number }로 제한했기 때문에 ab 매개변수의 .length 프로퍼티에 접근할 수 있었어요. 타입 제약이 없었다면 이 값들이 length 프로퍼티가 없는 다른 타입일 수도 있으니 접근할 수 없었을 거예요.

longerArraylongerString의 타입은 인자에 따라 추론됐어요. 기억하세요, 제네릭은 결국 "같은 타입을 가진 두 개 이상의 값을 서로 연결하는 것"이에요. 마지막으로, 기대한 대로 longest(10, 100) 호출은 거부되는데, number 타입에는 .length 프로퍼티가 없기 때문이에요.

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

제네릭 제약을 다룰 때 흔히 만나는 오류가 있어요:

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이거나 그 제약을 만족하는 값을 반환하니까요. 그런데 문제는 함수가 "전달받은 것과 같은 종류의 객체"를 반환하겠다고 약속한다는 점이에요. 제약을 만족하는 아무 객체가 아니라요. 이 코드가 허용된다면, 분명히 동작하지 않을 코드를 작성할 수 있게 돼요:

// '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);
}

일반적으로 이 함수를 타입이 다른 배열들로 호출하면 오류가 나요:

const arr = combine([1, 2, 3], ["hello"]);

그런데 의도적으로 이렇게 하고 싶다면 Type을 직접 지정할 수 있어요:

const arr = combine<string | number>([1, 2, 3], ["hello"]);

좋은 제네릭 함수를 쓰는 요령 (Guidelines for Writing Good Generic Functions)

제네릭 함수를 작성하는 건 재미있는데, 타입 매개변수에 너무 욕심내기 쉬워요. 타입 매개변수가 지나치게 많거나, 필요 없는 곳에 제약을 쓰면 추론이 잘 안 되고 함수를 호출하는 사람이 답답해져요.

타입 매개변수는 아래로 밀어내세요 (Push Type Parameters Down)

겉보기에는 비슷해 보이는 두 가지 작성 방식이 있어요:

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] 표현식을 제약 타입을 기준으로 풀어야 하니, 호출 시점에 요소 타입을 "기다려서" 풀지 못하기 때문이에요.

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

타입 매개변수는 더 적게 쓰세요 (Use Fewer Type Parameters)

비슷해 보이는 함수 쌍이 하나 더 있어요:

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는 함수를 더 읽기 어렵고 추론하기 어렵게 만들 뿐이에요!

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

타입 매개변수는 두 번 등장해야 해요 (Type Parameters Should Appear Twice)

가끔 우리는 함수가 제네릭일 필요가 없다는 사실을 잊어버려요:

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

greet("world");

더 단순한 버전으로도 충분히 쓸 수 있었죠:

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

기억하세요, 타입 매개변수는 여러 값의 타입을 서로 연결하는 데 쓰는 거예요. 타입 매개변수가 함수 시그니처에서 딱 한 번만 쓰인다면, 아무것도 연결하고 있지 않은 거예요. 여기에는 추론된 반환 타입도 포함돼요. 예를 들어 Strgreet의 추론된 반환 타입의 일부였다면, 인자 타입과 반환 타입을 연결하는 셈이 되니 코드에는 한 번만 등장해도 두 번 사용된 거나 마찬가지예요.

규칙: 타입 매개변수가 한 위치에만 등장한다면, 정말 필요한지 다시 한 번 생각해보세요.

선택적 매개변수 (Optional Parameters)

자바스크립트 함수는 가변 개수의 인자를 받는 경우가 많아요. 예를 들어 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가 돼요. 자바스크립트에서 지정되지 않은 매개변수는 undefined 값을 갖기 때문이에요.

매개변수에 기본값을 줄 수도 있어요:

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

이제 f의 함수 본문에서 xnumber 타입이 돼요. undefined 인자는 모두 10으로 대체되니까요. 참고로 매개변수가 선택적이면 호출자는 언제나 undefined를 넘길 수 있어요. 그냥 "인자를 빼먹은" 것처럼 동작하니까요:

// All OK
f();
f(10);
f(undefined);

콜백에서의 선택적 매개변수 (Optional Parameters in Callbacks)

선택적 매개변수와 함수 타입 표현식을 배우고 나면, 콜백을 호출하는 함수를 쓸 때 이런 실수를 하기 쉬워요:

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

index?를 선택적 매개변수로 쓴 사람들이 보통 의도하는 건, 아래 두 호출이 모두 합법이길 바라는 거예요:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

그런데 실제 의미는 이게 아니에요. 이건 callback이 인자 하나로 호출될 수도 있다는 뜻이에요. 다시 말해, 함수 정의가 "구현이 이렇게 생겼을 수도 있다"고 선언하는 거예요:

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는 이 의미를 강제하면서, 실제로는 있을 수 없는 오류를 내기도 해요:

myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});

자바스크립트에서는 매개변수보다 많은 인자로 함수를 호출하면, 추가 인자는 그냥 무시돼요. TypeScript도 똑같이 동작해요. (같은 타입의) 매개변수가 더 적은 함수는 항상 매개변수가 더 많은 함수를 대신할 수 있어요.

규칙: 콜백용 함수 타입을 쓸 때는, 그 인자 없이 그 함수를 호출할 의도가 아니라면 절대 선택적 매개변수를 쓰지 마세요.

함수 오버로드 (Function Overloads)

일부 자바스크립트 함수는 다양한 개수와 타입의 인자로 호출될 수 있어요. 예를 들어 Date를 만드는 함수라면 타임스탬프 하나를 받거나, 월/일/년 세 개를 받을 수 있겠죠.

TypeScript에서는 오버로드 시그니처(overload signature)를 써서 "다른 방식으로 호출될 수 있는 함수"를 지정해요. 이를 위해 함수 시그니처를 몇 개 쓰고(보통 두 개 이상) 그 뒤에 함수 본문을 이어 붙여요:

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);

이 예제에서 오버로드 두 개를 썼어요. 하나는 인자 하나를, 또 하나는 인자 세 개를 받아요. 이 첫 두 시그니처를 오버로드 시그니처라고 불러요.

그 다음에는 호환되는 시그니처로 구현 함수를 작성했어요. 함수에는 구현 시그니처(implementation signature)가 있는데, 이 시그니처는 직접 호출할 수 없어요. 필수 인자 뒤에 선택적 매개변수 두 개를 갖는 함수를 썼지만, 이걸 인자 두 개로 호출할 수는 없다는 거예요!

오버로드 시그니처와 구현 시그니처 (Overload Signatures and the Implementation Signature)

이건 흔한 혼동 지점이에요. 사람들이 이런 코드를 쓰면서 왜 오류가 나는지 이해하지 못할 때가 많아요:

function fn(x: string): void;
function fn() {
  // ...
}
fn();

다시 말하지만, 함수 본문을 작성할 때 쓴 시그니처는 바깥에서 "볼 수 없어요". 구현의 시그니처는 외부에서 보이지 않아요. 오버로드된 함수를 쓸 때는 항상 함수 구현 위에 시그니처를 두 개 이상 두어야 하고, 구현 시그니처는 오버로드 시그니처와 호환되어야 해요. 아래 함수들은 구현 시그니처가 오버로드와 올바르게 맞지 않아 오류가 나요:

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

좋은 오버로드 작성하기 (Writing Good Overloads)

제네릭처럼 함수 오버로드에도 따라야 할 요령이 몇 가지 있어요. 이 원칙들을 지키면 함수가 호출하기도, 이해하기도, 구현하기도 쉬워져요.

문자열이나 배열의 길이를 반환하는 함수를 생각해볼게요:

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

이 함수는 문제없어요. 문자열이나 배열로 호출할 수 있으니까요. 그런데 문자열일 수도 있고 배열일 수도 있는 값으로는 호출할 수 없어요. TypeScript는 함수 호출을 단 하나의 오버로드로만 해석할 수 있기 때문이에요:

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);

두 오버로드의 인자 개수와 반환 타입이 같으니, 오버로드 없는 버전으로 쓸 수도 있어요:

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

이 방식이 훨씬 낫죠! 호출자는 두 종류의 값으로 이 함수를 호출할 수 있고, 덤으로 올바른 구현 시그니처를 고민하지 않아도 돼요.

가능하면 오버로드 대신 유니언 타입의 매개변수를 항상 선호하세요.

함수에서 this 선언하기 (Declaring this in a Function)

TypeScript는 코드 흐름 분석을 통해 함수 안에서 this가 무엇이어야 하는지 추론해요. 예를 들면 이렇게요:

const user = {
  id: 123,
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

TypeScript는 user.becomeAdmin 함수에 외부 객체 user를 가리키는 this가 대응된다는 걸 이해해요. this는 많은 경우 충분하지만, this가 나타내는 객체를 더 세밀하게 제어해야 하는 경우도 많아요. 자바스크립트 명세상 this라는 이름의 매개변수는 쓸 수 없으므로, TypeScript는 그 문법 공간을 활용해서 함수 본문 안에서 this의 타입을 선언하게 해줘요:

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

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

이 패턴은 콜백 스타일의 API에서 흔해요. 다른 객체가 언제 함수를 호출할지 제어하는 경우죠. 이 동작을 얻으려면 화살표 함수가 아니라 function을 써야 한다는 점을 기억하세요:

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

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

알아두면 좋을 다른 타입들 (Other Types to Know About)

함수 타입을 다룰 때 자주 나타나는 타입이 몇 개 더 있어요. 모든 타입처럼 어디서나 쓸 수 있지만, 특히 함수라는 맥락에서 두드러지게 등장해요.

void

void는 값을 반환하지 않는 함수의 반환값을 나타내요. 함수에 return 문이 하나도 없거나, return 문에서 명시적인 값을 돌려주지 않을 때마다 추론되는 타입이에요:

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

자바스크립트에서 값을 반환하지 않는 함수는 암시적으로 undefined 값을 반환해요. 그런데 TypeScript에서 voidundefined는 같은 게 아니에요. 자세한 내용은 이 장의 마지막 부분에 있으니 거기서 다룰게요.

voidundefined와 같지 않아요.

object

특별한 타입 object는 원시 타입(string, number, bigint, boolean, symbol, null, 또는 undefined)이 아닌 어떤 값이든 가리켜요. 이건 빈 객체 타입인 { }와도, 전역 타입인 Object와도 달라요. 아마 Object는 쓸 일이 거의 없을 거예요.

objectObject가 아니에요. 항상 object를 쓰세요!

자바스크립트에서 함수 값은 객체라는 걸 기억해두세요. 프로퍼티를 갖고, 프로토타입 체인에 Object.prototype이 있고, instanceof Object가 성립하며, Object.keys를 호출할 수도 있어요. 그래서 TypeScript에서 함수 타입은 object로 취급돼요.

unknown

unknown 타입은 어떤 값이든 나타내요. any와 비슷하지만 더 안전해요. unknown 값으로는 아무것도 하는 게 합법적이지 않기 때문이에요:

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

이건 함수 타입을 묘사할 때 유용해요. 함수 본문에 any 값이 없이도 어떤 값을 받을 수 있는 함수를 표현할 수 있으니까요. 반대로 타입이 알려지지 않은 값을 반환하는 함수도 표현할 수 있어요:

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은 모든 자바스크립트 함수 값에 존재하는 bind, call, apply 같은 프로퍼티들을 설명해요. 그리고 Function 타입의 값은 항상 호출할 수 있다는 특별한 성질도 있는데, 이런 호출은 any를 반환해요:

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

이건 타입이 없는 함수 호출이라, 안전하지 않은 any 반환 타입 때문에 보통 피하는 게 좋아요. 임의의 함수를 받아야 하는데 호출할 의도는 없다면, () => void 타입이 일반적으로 더 안전해요.

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

배경 지식:

나머지 매개변수 (Rest Parameters)

선택적 매개변수나 오버로드로 고정된 개수의 인자를 받는 함수를 만드는 것 외에도, 나머지 매개변수(rest parameter)를 쓰면 인자 개수에 제한 없는 함수를 정의할 수 있어요.

나머지 매개변수는 다른 모든 매개변수 뒤에 나타나고, ... 문법을 사용해요:

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)

반대로, 이터러블 객체(예: 배열)에서 가변 개수의 인자를 전개 구문(spread syntax)으로 제공할 수도 있어요. 배열의 push 메서드는 인자를 몇 개든 받아요:

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

일반적으로 TypeScript는 배열이 불변이라고 가정하지 않아요. 그래서 의외의 동작이 생길 수 있어요:

// 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)

배경 지식:

매개변수 구조 분해(destructuring)를 쓰면 인자로 전달된 객체를 함수 본문의 지역 변수 하나 이상으로 편리하게 풀어낼 수 있어요. 자바스크립트에서는 이렇게 생겼죠:

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인 문맥적 타이핑(contextual typing)은 함수가 무언가를 반환하지 않도록 강제하지 않아요. 다시 말하면, void 반환 타입의 문맥적 함수 타입(type voidFunc = () => void)은 구현할 때 다른 어떤 값을 반환해도 되지만, 그 값은 무시된다는 뜻이에요.

따라서 () => void 타입의 다음 구현들은 모두 유효해요:

type voidFunc = () => void;

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

const f2: voidFunc = () => true;

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

그리고 이 함수들 중 하나의 반환값을 다른 변수에 할당하면, 그 변수는 void 타입을 유지해요:

const v1 = f1();

const v2 = f2();

const v3 = f3();

이 동작이 존재하는 이유는, 다음 코드가 유효하도록 하기 위해서예요. Array.prototype.pushnumber를 반환하는데, 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에 대해 더 자세히 알고 싶다면 아래 문서 항목을 참고하세요:

더 알아보기