공부기록/웹 개발

[타입스크립트] 자주쓰는 메소드 타이핑

_우지 2022. 10. 6. 19:50

forEach

우선 forEach 부터 시작해봅시다.

Arr 타입에 대한 forEach 메소드의 타입을 정의해주어야합니다.

interface Arr {}

const a: Arr = [1, 2, 3];
a.forEach((item) => {
  console.log(item);
});

a.forEach((item) => {
  console.log(item);
  return 3;
});

 

 

우선 forEach는 리턴값이 없기때문에 void로 정의해줍니다. 이 함수는 아무것도 반환하지 않는다는 의미입니다.

interface Arr {
  forEach(): void;
}

 

 

그 다음으로는 forEach 내부의 callback 의 타입을 지정해주어야합니다.

우선 callback 또한 리턴을 void 로 선언했습니다. 콜백의 리턴이 void 인것은 후에 수정을 해줘야할 것 같습니다.

interface Arr {
  forEach(callback: () => void): void;
}

 

 

위와 같이 타입을 정의해주면 아래와 같은 에러메세지를 볼 수 있었습니다.

콜백안의 파라미터의 타입을 정의해주라고 합니다.

 

 

다음과 같이 콜백안의 파라미터의 타입을 정의해주었습니다. 우선 number로 정의했습니다.

interface Arr {
  forEach(callback: (item: number) => void): void;
}

 

 

하지만 위와 같이 타입을 정해준다면 item이 number 인 경우밖에 커버할 수 없습니다.

이럴때 사용해야하는 것이 제네릭입니다.

제네릭을 넣어줘야하는 위치 interface Arr 뒤 입니다. 다음과 같습니다.

  • 타입 바로 뒤
  • interface Arr<T> { forEach(callback: (item: T) => void): void; }

 

이미 js 메소드에 대한 타입이 정리된 d.ts 파일 을 보던중에 thisArg 라는 파라미터가 뭔지 궁금했습니다.

곰곰히 생각해본 결과 이전에 eventListener 와 같은 코드를 콜백으로 넣을경우 this 가 window 일때 원하던대로 동작하지 않던 경우가 떠올랐습니다. 그것에 대한 옵션인것 같습니다.

  forEach(
    callbackfn: (value: T, index: number, array: T[]) => void,
    thisArg?: any
  ): void;

 

 

어렵다고 생각했는데 별거 없는 것 같습니다.

 

map

interface Arr<T> {
  forEach(callback: (item: T, index: number, array: T[]) => void): void;
  map(callback: (el: T) => void): void;
}

const a: Arr<number> = [1, 2, 3];

const b = a.map((el) => el + 1);
console.log(b); // 잘 나오지만 b 의 타입이 void 였다.

 

 

아래 처럼 코드르 수정해야 b 의 타입이 void 가 아닌 number[] 가 됩니다.

interface Arr<T> {
  forEach(callback: (item: T, index: number, array: T[]) => void): void;
  map(callback: (el: T) => T): T[];
}

const a: Arr<number> = [1, 2, 3];

const b = a.map((el) => el + 1);
console.log(b); // [1,2,3]

 

 

그런데 다음처럼 element 를 string으로 변경할 경우 타입의 문제가 생깁니다.

왜냐하면 콜백의 반환이 T 제네릭으로 선언되어있습니다. 따라서 현재 number type에 elements 에 toString 메소드를 적용하려니 에러가 납니다.

이런 경우에는 제네릭을 하나더 써주어야합니다.

다음과 같이 말이죠.

<S> 라는 제네릭을 map 뒤에 추가해줌으로써 좀 더 유연한 타입정의가 가능하게 되었습니다.

동작 순서를 설명하면 다음과 같습니다.

  1. el.toString()이 string 타입이므로 S = string 이 됩니다.
  2. 나머지 S에 string 이 정의되어 map<S> , 함수의 반환도 string[] 가 됩니다.
interface Arr<T> {
  forEach(callback: (item: T, index: number, array: T[]) => void): void;
  map<S>(callback: (el: T) => S): S[];
}

const a: Arr<number> = [1, 2, 3];

const b = a.map((el) => el + 1);
const c = a.map((el) => el.toString());

 

filter

우선 기본 세팅은 다음 처럼 됩니다.

interface Arr<T> {
  filter(callback: (el: T) => void): void;
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const b = a.filter((el) => typeof el === "string");

 

 

filter 타입은 다음과 같습니다.

음 예상 되는 결과는 string[] 이여하는데 b는 void 타입인 것을 확인했습니다.

 

이렇게 바꿔줘 봤습니다.

interface Arr<T> {
  filter(callback: (el: T) => boolean): T[];
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const b = a.filter((el) => typeof el === "string");

 

 

타입은 다음과 같네요.

 

하나의 제네릭 가지고는 string[] 을 만들 수가 없습니다. 따라서 U 라는 제네릭을 추가합니다.

 

저는 이 과정에서 다음과 같은 타입스크립트 타이핑으로 완료가 되었다 라고 생각했습니다.

interface Arr<T> {
  forEach(callback: (item: T, index: number, array: T[]) => void): void;
  map<S>(callback: (el: T) => S): S[];
  filter<U>(callback: (el: T) => boolean): U[];
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const b = a.filter<string>((el) => typeof el === "string");
const c = a.filter<number>((el) => typeof el === "number");
console.log(b);
console.log(c);

 

하지만 lib.es5.d.ts 를 보니까 다음처럼 타이핑이 되었더라구요. 일단 아래 코드를 분석해보겠습니다.

우선 string | number 라는 범위를 좁히기 위해서 커스텀 타입 가드를 사용한 모습입니다.

그래서 저도 타입가드를 사용하기 위해 제 코드를 커스텀 타입가드를 사용하는 상태로 바꾸어보았습니다.

interface Arr<T> {
  filter<U>(callback: (el: T) => el is U): U[];
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const b = a.filter<string>((el) => typeof el === "string");
const c = a.filter<number>((el) => typeof el === "number");
console.log(b);
console.log(c);

 

다음과 같은 에러를 만났습니다.

제네릭 U 와 제네릭 T 가 아무런 연관이 없다고 합니다.

범위를 좁힌다는 것은 두 제네릭 간의 어떤 연관 관계 가 있을때 교집합으로 범위를 좁힌다는 뜻인데

전혀 교점이 없다는 뜻입니다.

 

따라서 extends 를 사용해서 두 제네릭 간의 연관관계를 만들어줍니다.

다음 처럼 말이죠. 이해하시기 쉽게 밴다이어그램을 그려서 표현해보겠습니다.

  filter<U extends T>(callback: (el: T) => el is U): U[];

T 는 string 과 number 타입이 유니온으로 묶여있기 때문에 위 처럼 밴다이어그램이 그려질 겁니다.

U 는 저 밴다이어그램의 부분집합 어딘가가 되겠습니다. string , number 가 될 수 있겠네요.

 

 

interface Arr<T> {
  filter<U>(callback: (el: T) => el is U): U[];
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const b = a.filter<string>((el) => typeof el === "string");
const c = a.filter<number>((el) => typeof el === "number");
console.log(b);
console.log(c);

위와 같이 타이핑을 했는데 다음 에러를 만났습니다.

signature로 만든 커스텀 타입가드를 함수 리턴에 사용하라는 것이네요.

 

 

다음처럼 커스텀 타입가드를 적용해주었습니다.

커스텀 타입가드의 역할은 큰 범위의 타입 값을 좁은 범위로 좁혀주는 것입니다.

const b = a.filter((el): el is string => typeof el === "string");
const c = a.filter((el): el is number => typeof el === "number");

 

 

filter 안의 타입을 다음처럼 빼줄 수도 있습니다.

interface Arr<T> {
  filter<U extends T>(callback: (el: T) => el is U): U[];
}

const a: Arr<string | number> = [1, "2", 2, "3", 3];
const predicate = (v: string | number): v is number => typeof v === "number";
const c = a.filter(predicate);
console.log(c);

predicate 로 뺀 함수는 커스텀 타입이 number 로 하드 코딩이 되어있는 상태여서, 조금더 유연하게 끔 변수를 할당을 하고 싶었습니다. 따라서 predicate 변수를 return 하는 함수를 만들었습니다.

 

 

정리를 하면 다음과 같습니다.

  • 커스텀 타입가드를 사용해서 큰 범위의 타입값을 좁은 범위로 좁혀줄 수 있다.
  • 제네릭을 두개 사용할 경우 관계를 엮기 위해 extends 를 사용한다.

 

참고자료

https://developer-talk.tistory.com/359

[[TypeScript]함수 타입(Function Type)

TypeScript에서 함수 정의 TypeScript의 함수는 JavaScript처럼 함수를 생성할 수 있지만, 매개변수의 타입과 반환 타입을 설정해야 합니다. 다음은 number 타입의 매개변수와 string 값을 반환하는 getParam()..

developer-talk.tistory.com](https://developer-talk.tistory.com/359)