blog

TypeScript에서 파라미터 개수가 다른 함수가 같은 타입에 들어갈 수 있는 이유

Front-end / TypeScript#TypeScript#함수 타입#타입 호환성#콜백

상황

mutation 팩토리에 이런 옵션을 정의했다.

type ApiMutationConfig<TData, TVariables> = {
  invalidateKeys?: (
    data: CustomData<TData>,
    variables: TVariables,
  ) => readonly (readonly unknown[])[];
};

파라미터 2개짜리 함수 타입인데, 실제로는 파라미터 개수가 다른 함수들이 문제없이 들어갔다.

// 파라미터 0개
invalidateKeys: () => [ASSETS_KEY],

// 파라미터 2개
invalidateKeys: (_: unknown, { groupSeq }: { groupSeq: number }) => [
  queryKeys.codeItems(groupSeq),
  queryKeys.codeGroupList(),
],

이유: 적게 받는 함수는 많이 받는 함수 자리에 들어갈 수 있다

함수 타입 (data, variables) => ...는 "호출하는 쪽이 인자 2개를 넘겨준다"는 약속이다. "함수가 반드시 2개를 다 받아야 한다"는 강제가 아니다. 파라미터를 더 적게 선언한 함수는 넘어온 인자 중 일부를 무시할 뿐이므로 약속을 어기지 않는다.

이는 JavaScript의 실제 동작과 같다.

const f = () => 'hi';
f(1, 2, 3); // 'hi' - 남는 인자는 그냥 무시됨

우리가 매일 쓰는 코드도 이 규칙 덕분에 동작한다. Array.prototype.map의 콜백 타입은 (value, index, array) => U지만, 대부분 value 하나만 받는 함수를 넘긴다.

[1, 2, 3].map((n) => n * 2); // index, array를 선언하지 않아도 OK

반대 방향은 안 된다

함수가 필수로 요구하는 인자보다 적게 넘겨 호출하면 에러다.

const invalidateCodeKeys = (_: unknown, { groupSeq }: { groupSeq: number }) => [/* ... */];

invalidateCodeKeys(data); // Error: Expected 2 arguments, but got 1.

그래서 타입이 약속한 개수보다 필수 파라미터를 더 많이 선언한 함수는 그 자리에 들어갈 수 없다. 호출하는 쪽은 2개만 넘기므로 세 번째 인자가 항상 비게 되기 때문이다.

type Fn = (a: number, b: number) => void;

const ok1: Fn = () => {};                         // ✅ 0개
const ok2: Fn = (a: number) => {};                // ✅ 1개
const ok3: Fn = (a: number, b: number, c?: number) => {}; // ✅ 3번째가 선택적이면 OK
const ng: Fn = (a: number, b: number, c: number) => {};   // ❌ 3번째가 필수

팩토리 내부는 항상 약속대로 2개를 넘겨 호출한다.

const keys = invalidateKeys?.(data, variables) ?? [];

들어온 함수가 그중 일부만 쓰든 전혀 쓰지 않든 그것은 함수의 자유다.

정리

대상 자리의 타입넣으려는 함수결과
(a, b) => R파라미터 0~2개✅ 남는 인자 무시
(a, b) => R3번째 파라미터가 선택적✅
(a, b) => R3번째 파라미터가 필수❌ 호출 시 인자 부족

규칙은 "함수의 필수 파라미터 수가 호출자가 약속한 인자 수 이하"다. 덕분에 같은 invalidateKeys 옵션 하나로 "고정 키만 무효화"와 "호출 시점 값으로 동적 키 무효화"를 모두 표현할 수 있었다.

참고