숏컷 코드
type Ready = () => void;
type Formatter = (value: number) => string;
type Join = (...parts: string[]) => string;
function formatPrice(value: number, fn: Formatter) {
return fn(value);
}함수 계약
함수 타입에서 먼저 봐 둘 기본형은 아래입니다.
- 인수가 없는 함수:
() => void - 일반 함수 타입:
(value: number) => string - 반환값을 쓰지 않는 함수:
(message: string) => void - 선택 인수:
(name: string, title?: string) => string - rest 인수:
(...parts: string[]) => string - 콜백 별칭:
type Listener = (...) => ... - 제네릭 함수 타입:
<T>(value: T) => T
1) 입력과 출력의 계약을 보여주면 함수 타입
함수 타입은 "무슨 값을 받아서 무엇을 돌려주는가"를 주석이 아니라 타입 시스템 안에 넣는 방법입니다.
type Compare = (left: string, right: string) => number;이렇게 타입을 분리해 두면 호출부와 구현부가 같은 계약을 보게 됩니다.
2) 반복되는 콜백 시그니처는 별도 타입으로 뽑는다
이벤트 리스너, 포매터, 매퍼처럼 같은 함수 shape가 반복되면 익명 시그니처를 매번 적기보다 type으로 뽑는 편이 읽기 쉽습니다.
type Listener = (event: string) => void;
function subscribe(listener: Listener) {
// ...
}즉 콜백 타입 분리는 재사용뿐 아니라 API 읽기성도 올려 줍니다.
3) void는 결과를 사용하지 않는다는 신호다
void는 "반드시 아무것도 반환하지 않는다"보다, 호출자가 반환값을 기대하지 않는다는 계약에 가깝습니다.
function log(message: string): void {
console.log(message);
}이 점이 잡혀 있으면 side effect 함수와 계산 함수를 구분하기 쉬워집니다.
4) 매개변수 형태 자체가 호출 계약을 바꾼다
인수가 아예 없는지, 일부를 생략할 수 있는지, 여러 개를 묶어서 받는지까지 모두 함수 계약의 일부입니다.
const ready: Ready = () => {
console.log("ready");
};
function greet(name: string, title?: string) {
return title ? `${title} ${name}` : name;
}
function joinParts(...parts: string[]) {
return parts.join("/");
}즉 함수 타입 설계는 단순 타입 표기보다 "이 함수를 어떻게 부를 수 있는가"를 정하는 일입니다.
ready();
greet("Mina");
greet("Mina", "Dr.");
joinParts("docs", "typescript", "functions");type OnData = (chunk: string, index: number) => void;
function readAll(chunks: string[], onData: OnData) {
chunks.forEach((chunk, index) => onData(chunk, index));
}선택 매개변수와 콜백 시그니처를 같이 보면, 호출자가 무엇을 받을 수 있는지를 타입이 직접 설명한다는 감각이 더 잘 잡힙니다.
5) 콜백에 any를 두면 전체 API가 빠르게 흐려진다
콜백 계약이 모호하면 사용하는 쪽도, 구현하는 쪽도 타입 이점을 잃습니다. 그래서 콜백 시그니처는 가능한 한 입력과 출력 관계를 명확히 잡는 편이 좋습니다.
type Mapper = <T, R>(value: T, map: (input: T) => R) => R;입력 타입과 출력 타입의 관계를 보존해야 하는 콜백이라면, 제네릭 함수 타입까지 같이 검토하는 편이 좋습니다.
언제 별도 타입을 뽑을까
함수 타입을 설계할 때 볼 점
- 함수 입력/출력 계약을 보여준다: 함수 타입
- 같은 콜백 시그니처가 반복된다: 별도
type - 반환값을 쓰지 않는다:
void - 인수 모양이 다르다: 없음 / 선택 / rest 매개변수를 같이 본다
- 콜백에
any가 들어간다: 계약을 더 좁힐지 다시 본다
주의할 점
콜백에 any를 두면 그 지점부터 타입 안전성이 급격히 약해집니다. 콜백 계약이 모호한 API는 사용하는 쪽도 구현하는 쪽도 불안정해집니다.
// ❌ 콜백 계약이 흐려짐
function mapItems(items: any[], fn: (value: any) => any) {
return items.map(fn);
}
// ✅ 콜백 계약을 타입으로 고정
function mapItems<T, R>(items: T[], fn: (value: T) => R): R[] {
return items.map(fn);
}// ❌ optional parameter를 "항상 온다"처럼 쓰면 호출부와 구현이 어긋난다
function greet(name: string, title?: string) {
return title.toUpperCase() + name;
}
// ✅ optional이면 undefined 경로도 같이 처리
function greetSafe(name: string, title?: string) {
return title ? `${title.toUpperCase()} ${name}` : name;
}참고 링크
1 sources