개발새발
forEach 만들기 본문
배열에는 잘 알려진 메서드인 forEach가 존재한다. 평소에는 아무렇지 않게 사용하던 이 메서드도, 막상 타입을 직접 선언해보라고 하면 당황하기 쉽다. 평소에 직접 메서드를 타이핑하는 연습을 하지 않았기 때문이다.
이번 글에서는 TypeScript의 배열 메서드 forEach와 동일한 기능을 하는 myForEach라는 메서드를 직접 타이핑하고 구현하며, 타입 선언의 원리와 과정을 배워보자.
먼저 아래와 같이 myForEach를 호출해보자.
[1, 2, 3].myForEach(() => {});
위 코드를 작성하면 다음과 같은 에러가 발생한다.
Property 'myForEach' does not exist on type 'number[]'.
이는 당연한 결과이다. myForEach는 존재하지 않기 때문이다.
배열 인터페이스 확장
TypeScript의 Array는 인터페이스로 정의되어 있기 때문에, 동일한 이름의 인터페이스를 선언하면 병합(Declaration Merging)이 가능하다. 이를 이용해 아래와 같이 선언한다.
interface Array<T> {
myForEach(): void;
}
그러나 이 상태에서 다시 호출하면 또 다른 에러가 발생한다.
Expected 0 arguments, but got 1.
이는 myForEach에 인자가 정의되어 있지 않기 때문이다. 따라서 콜백을 받을 수 있도록 수정하자.
interface Array<T> {
myForEach(callback: () => void): void;
}
이제 인자 수 관련 에러는 사라지지만, 다양한 테스트를 해보면 여전히 문제가 많다.
콜백 함수의 매개변수 타이핑
다양한 형태의 콜백을 넣어 테스트해보면 다음과 같은 에러가 발생한다.
[1, 2, 3].myForEach((v, i, a) => console.log(v, i, a));
[1, 2, 3].myForEach((v) => 3); // 'number' is not assignable to 'void'
이는 콜백 함수의 매개변수가 전혀 선언되어 있지 않거나, 반환 타입이 void가 아닌 경우 발생하는 에러이다. forEach의 콜백은 (value, index, array) 형태로 총 3개의 매개변수를 받으며, 반환값은 없다.
다음과 같이 수정하자.
interface Array<T> {
myForEach(callback: (v: T, i: number, a: T[]) => void): void;
}
이제 대부분의 테스트 케이스가 통과하게 된다.
다양한 타입에 대한 대응
이전 예시는 number[] 배열에만 유효했다. string[], boolean[], 혹은 혼합된 타입의 배열을 넣었을 때도 타입이 제대로 작동해야 한다.
예를 들어 다음과 같은 코드를 보자.
['a', 'b', 'c'].myForEach((v) => v.slice(0));
[true, 2, '3'].myForEach((v) => {
if (typeof v === 'string') {
v.slice(0); // OK
} else {
v.toFixed(); // Error → boolean에는 toFixed가 없음
}
});
이러한 문제는 v의 타입이 고정(number)되어 있었기 때문에 발생한 것이다. 이를 제네릭 타입 T로 수정하면 모든 배열 타입에 유연하게 대응할 수 있다.
thisArg까지 타이핑하기
TypeScript의 lib.es5.d.ts에 정의된 원래의 forEach는 다음과 같이 thisArg 매개변수를 제공한다.
interface Array<T> {
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
}
콜백 함수에서 this를 명시적으로 사용할 수 있도록 thisArg 타입도 지정해야 한다. 이를 위해 콜백 함수에 this 타입 매개변수도 추가해야 한다.
최종적으로 myForEach의 타입은 다음과 같이 정의할 수 있다.
interface Array<T> {
myForEach<K = Window>(
callback: (this: K, v: T, i: number, a: T[]) => void,
thisArg?: K
): void;
}
- K = Window는 기본 this 타입을 브라우저 환경 기준으로 설정한 것이다.
- thisArg를 넘기면 K가 해당 타입으로 지정되며, 콜백 내부의 this 타입으로 반영된다.
실행되지 않는 이유
현재까지는 타입 선언만 완료한 상태이다. 실제 myForEach를 구현한 적이 없기 때문에 JavaScript에서는 실행되지 않는다. 즉, 타입스크립트의 타입 검사기는 통과하더라도, 실제 런타임에서는 다음과 같은 에러가 발생한다.
TypeError: [1,2,3].myForEach is not a function
이를 방지하려면 실제 메서드를 프로토타입에 구현해줘야 한다.
Array.prototype.myForEach = function <T, K = Window>(
this: T[],
callback: (this: K, v: T, i: number, a: T[]) => void,
thisArg?: K
): void {
for (let i = 0; i < this.length; i++) {
callback.call(thisArg as K, this[i], i, this);
}
};
타입스크립트의 타입 선언은 단순히 타입 오류를 방지하기 위한 도구가 아니다. 타입을 직접 선언해보는 과정은 해당 함수가 어떻게 동작하는지, 어떤 인자를 받고 어떤 값을 반환하는지, 런타임의 this가 어떻게 결정되는지를 깊이 있게 이해하는 데 도움을 준다.
실제로 TypeScript가 제공하는 forEach의 타입조차 완벽하지 않다. 실행 환경에 따라 this가 window, global, undefined 등으로 달라지기 때문이다. 이처럼 100% 완벽한 타입 선언은 존재하지 않을 수 있으며, 다양한 테스트 사례를 통해 "충분히 잘 만든 타입"을 만들어내는 것이 현실적인 목표이다.
요약
- Array 인터페이스는 병합이 가능하므로 직접 확장할 수 있다.
- 콜백 함수의 인자 수와 타입을 정확하게 타이핑해야 한다.
- 다양한 테스트 사례가 필요하다.
- thisArg와 this의 타입도 선언할 수 있다.
- 타입 선언만으로는 실제 동작하지 않으므로 구현도 필요하다.
'Typescript' 카테고리의 다른 글
filter 만들기 (0) | 2025.04.30 |
---|---|
map 만들기 (1) | 2025.04.30 |
ThisType (0) | 2025.04.12 |
Parameters, Constructorparameters, ReturnType, InstanceType (0) | 2025.04.12 |
Exclude, Extract, Omit, NonNullable (1) | 2025.04.12 |