본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

가변 길이 튜플: 인자 목록의 타입을 통째로 보존해

~11 min · arrays-tuples, variadic-tuples, generics

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"가변 길이 튜플은 함수 인자 목록을 ‘아무 값의 배열’이 아니라 위치별 타입을 가진 하나의 모양으로 붙잡아."

튜플 안의 펼침

[string, ...number[]]은 첫 칸이 문자열이고 뒤에는 숫자가 몇 개든 이어지는 튜플이야. 펼침 자리에 구체 배열뿐 아니라 제네릭 튜플 매개변수를 넣을 수 있다는 점이 핵심이야. [Head, ...Tail]처럼 앞이나 뒤에 고정 요소를 더할 수도 있어.

함수의 나머지 매개변수는 본래 인자 목록의 튜플로 볼 수 있어. Args extends unknown[]를 붙잡고 (...args: Args)로 받으면 각 위치의 타입과 인자 개수를 함께 보존한다.

래퍼 함수에서 정보 손실을 막아

어떤 함수든 받아 호출 전후에 로그를 남기는 래퍼를 생각해 봐. 인자를 any[]로 받으면 원래 함수의 매개변수 정보가 사라져. 같은 Args를 원래 함수와 반환 함수의 나머지 매개변수에 쓰면 호출자는 원래와 똑같은 자동 완성과 검사를 얻는다.

반환 타입도 별도 제네릭 R로 보존하면 래퍼를 거쳐도 함수 계약이 그대로 살아. 로깅, 재시도, 측정 같은 공통 기능을 넣으면서 타입 표면을 잃지 않는 방법이야.

앞뒤에 인자를 더하고 빼기

[requestId: string, ...Args]처럼 고정 인자를 앞에 붙인 새 호출 계약을 만들 수 있어. 조건부 타입과 infer를 결합하면 튜플의 머리와 꼬리를 분리하거나 일부 인자를 미리 채운 함수의 남은 인자 타입을 계산할 수 있다.

오버로드 폭발을 줄여

예전에는 인자 한 개부터 여덟 개까지 각각 오버로드를 적던 compose나 partial 함수가 많았어. 가변 길이 튜플은 하나의 제네릭 시그니처로 임의 길이를 다루게 해. 다만 여러 함수의 입출력을 끝없이 연결하는 타입은 여전히 복잡해질 수 있으니 읽기 어려운 마법을 목표로 삼지는 마.

가변 길이 튜플은 TypeScript 4.0에서 추가됐어. 이 기능 덕분에 현대 TypeScript의 curry, partial, compose 계열 함수가 임의 길이의 인자 목록을 위치별 타입까지 보존할 수 있어.

인자 목록을 전달하거나 변형하는 함수라면 목록 전체를 튜플 제네릭으로 붙잡아. 개수와 위치의 정보가 끝까지 살아남는다.

피파의 고백

매일 쓰는 문법은 아니지만 래퍼 하나가 any를 퍼뜨리기 시작하는 순간 대체할 도구가 거의 없어. 드문 기능은 외울 필요보다 알아볼 필요가 먼저야. 필요할 때 이름이 떠오르면 문서를 찾아 정확히 쓸 수 있으니까.

Code

Tuple 통해 generic argument forward·typescript
// 패턴 1: argument 변경 없이 forward.

function tap<Args extends unknown[]>(
  fn: (...args: Args) => void,
  ...args: Args
): void {
  fn(...args);
}

tap((a: number, b: string) => console.log(a, b), 1, 'hi');     // ✅
tap((a: number, b: string) => console.log(a, b), 1, 2);        // ❌ 두 번째 arg 가 string 이어야

// Compiler 가 앎: spread args 가 callback signature 와 match.
// Variadic tuple 없으면 arity N 까지 overload 필요.
Variadic + infer 로 tuple 모양 변환·typescript
// 패턴 2: 고정 위치 prepend.

type WithRequestId<Args extends unknown[]> = [requestId: string, ...args: Args];

function withRequestId<Args extends unknown[]>(
  fn: (...args: Args) => void,
) {
  return (...wrapped: WithRequestId<Args>) => {
    const [requestId, ...rest] = wrapped;
    console.log(`[${requestId}]`);
    fn(...rest as Args);
  };
}

const logUser = withRequestId((name: string, age: number) => console.log(name, age));
logUser('req-1', 'Pippa', 21);     // ✅ requestId prepend, 원래 args 뒤따름

// 패턴 3: infer 로 head 와 tail 분리.
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];

type A = Head<[number, string, boolean]>;   // number
type B = Tail<[number, string, boolean]>;   // [string, boolean]

External links

Exercise

임의 함수의 인자를 그대로 전달하며 호출 전에 기록하는 logCall을 작성해. 이어서 두 함수를 합성하고 첫 함수의 반환 타입이 둘째 함수의 입력으로 연결되게 해.
Hint
wrap<Args extends unknown[], R>(fn: (...args: Args) => R)는 같은 Args 튜플을 받는 래퍼 함수를 반환하게 해. pipe2는 첫 함수의 반환 타입과 둘째 함수의 입력 타입을 같은 제네릭으로 연결해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.