Function 써도 될까?

dante Yoon·2022년 12월 17일
5

js/ts

목록 보기
7/14
post-thumbnail

동영상으로 보기

글을 시작하며

안녕하세요, 단테입니다.

오늘은 타입스크립트의 Function 타입에 대해 알아보겠습니다.

함수를 선언하고 싶은데 간단하게 Function 타입을 사용하고 싶다.

문자열은 string, 숫자형은 정수든 부동 소수점이든 number, 참/거짓은 boolean.

함수도 왠지 Function이라는 타입으로 모든 함수 타입을 아우르면 좋을 것 같습니다.

있네?

타입스크립트에서는 Function 타입을 제공합니다.

interface Function {
    /**
     * Returns the name of the function. Function names are read-only and can not be changed.
     */
    readonly name: string;
}

사용해도 될까?

Function을 사용해서 선언한 mapper 함수와 그렇지 않은 함수를 비교해보겠습니다.

요구사항

저희는 string, number 를 argument로 받고 string 타입을 반환하는 mapper라는 함수를 선언하려고 합니다.

함수 시그니처를 선언해보겠습니다.

type Item =  string | number;
interface Mapper {
  (_item: Item): string
}

인터페이스가 아니라 타입으로 선언하면 다음과 같습니다.

type Mapper = (_item: Item) => string;

그리고 fn은 특정 유틸 함수로 쓰이고 콜백 함수로 쓰입니다.

예를 들면 reduce의 콜백함수로 쓰이는 것인데, 아래와 같은 유즈 케이스가 있겠네요.

Function을 사용하지 않은 함수

아래 join이라는 함수를 만들었습니다. 이 join 함수는 어떤 타입의 배열도 받아들일 수 있습니다. 그저 mapper 함수에서 string 타입을 반환하기만 하면 됩니다.

const array: Array<string | number> = ['a', 1, 'b', 2, 'c', 3];

const join = <T>(items: Array<T>, mapper: (_item: T) => string): string => {
  const concatted = items.reduce((acc: string, curr) => {
    return acc.concat(mapper(curr));
  }, '');
  
  return concatted;
};
join(array, (item) => item.toString()); // "a1b2c3"

앞서서 array 타입을 Array<string | number>이라고 구체적으로 선언했습니다.

그래서 join 함수 내부에 전달된 함수 파라메터 item의 타입은 string | number 타입이라고 추론이 잘 되죠.
자동추론 비사용

아직까지 별 특이점이 안보입니다.

array 타입 선언을 생략하겠습니다.

다음 코드에서는 위에서와 다르게 const array 의 타입을 생략하고 타입스크립트의 자동 추론에 의존합니다.

const array = ['a', 1, 'b', 2, 'c', 3];

그래도 동일하게 string | number로 추론이 잘 됩니다.

자동추론 사용

join 함수 선언에서 제너릭 T를 활용했기 때문에 파라메터로 넘기는 변수와 함수에 명시적인 타입선언을 해주지 않아도 됩니다.

Function 타입을 사용해보자.

다음 join2 함수는 두번쨰 함수 인자 타입으로 제너릭 타입이 아닌 Function 타입을 사용합니다.

const join2 = <T>(items: Array<T>, mapper: Function): string => {
  const concatted = items.reduce((acc: string, curr) => {
    return acc.concat(mapper(curr));
  }, '');
  return concatted;
};

정확하지 못한 타입 추론

동일한 array를 넘겼음에도 불구하고 mapper 함수 내부에서 item 타입을 제대로 추론하지 못합니다.

이를 위해 매번 넘기는 함수 타입을 지정해주어야 합니다.

하지만 이 떄 생기는 문제는 mapper 함수 타입과 array 타입 간의 연관 관계가 완전히 깨져버린다는 것입니다.

단순 정적 타입 오류를 해결하기 위해서는 다음 처럼 실제 array 타입 T와 무관하게 로직에 맞게 적당히 타입을 선언하면 됩니다.

실제 넘기는 array 타입은 string|number[]

파라메터 타입과 리턴 타입에 대한 아무런 정보를 주지 못하기 때문에 함수 역할에 대한 정보가 아무것도 전달되지 못합니다. 코드 사이즈가 커지면 커질 수록 이런 정보를 추측하기가 어려워지는 것입니다.

또한 실제 함수 mapper 타입이 number => string 이어야 함에도 string => number를 전달할 때 이에 대한 타입 에러를 컴파일 타임에 잡지 못합니다.

타입스크립트를 사용하는 이유가 없어지죠.

클래스 정의문을 인자로 넘길 때

특정 함수에서 클래스 정의문을 인자로 받아 함수 호출 시 해당 클래스를 호출한다고 가정해보겠습니다.

다음의 코드에서 myFunction은 Function 타입으로 선언이 되었고 내부 구현에서 MyClass를 사용할 때 항상 new 키워드를 사용해야 합니다. 해당 정보를 모른다면 myFunction을 구현하는 측에서 MyClass에 new키워드 붙이는 것을 생략할 수 있죠.

class MyClass {
  constructor(public name: string) {}
}

const myFunction: Function = (MyClass: any) => {
  const instance = new MyClass("John");
  console.log(instance.name);
};

myFunction(MyClass); // Output: "John"

이 때 다음과 같이 myFunction 구현부에서 new 키워드를 생략해버린다면 에러가 발생함에도 IDE에서 잡지 못합니다.

이를 해결하기 위해서는 myFunction 타입을 명시적으로 지정해주어야 합니다.

type MyFunction = (MyClass: new () => any) => void;

const myFunction: MyFunction = (MyClass: new () => any) => {
  const instance = new MyClass();
  console.log(instance.name);
};

myFunction(MyClass); // Output: "John"

eslint를 활용해 방지하자

typescript-eslint 의 ban-types 플러그인을 사용해 Function 타입 선언을 방지할 수 있습니다.

plugin:@typescript-eslint/recommended에 포함되어 있으므로 해당 플러그인을 extend 하면 됩니다.

// .eslintrc.cjs
module.exports = {
  "rules": {
    "@typescript-eslint/ban-types": "error"
  }
};

글을 마치며

오늘은 typescript에서

타입스크립트에서 함수 타입 선언시에는 실제 해당 함수를 사용하는 클라이언트 코드에서 파라메터와 리턴 타입을 추론 가능하게 선언해야 합니다. 이런 사항들이 보장되어야 가독성과 유지보수성, 디버깅을 하기 용이합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글