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) => R | 3번째 파라미터가 선택적 | ✅ |
(a, b) => R | 3번째 파라미터가 필수 | ❌ 호출 시 인자 부족 |
규칙은 "함수의 필수 파라미터 수가 호출자가 약속한 인자 수 이하"다. 덕분에 같은 invalidateKeys 옵션 하나로 "고정 키만 무효화"와 "호출 시점 값으로 동적 키 무효화"를 모두 표현할 수 있었다.