디스패치 테이블과 TS

디스패치 테이블 (opens in a new tab)

기본적으로 다중 if ... elseif ... elseswitch는 지양하는 편이다. 가독성도 좋지 않고 분기를 위한 코드가 많다고 느껴지기 때문이다.

계산기를 위한 분기문을 작성하는 예시를 통해 확인해 보자.

//Bad
 
type Operater = "+" | "-" | "*" | "/";
 
const calculate = (operator: Operater, a: number, b: number) => {
  if (operator === "+") return a + b;
  else if (operator === "-") return a - b;
  else if (operator === "*") return a * b;
  else if (operator === "/") return a / b;
  else throw new Error("Invalid operator");
};
 
const calculate = (operator: Operater, a: number, b: number) => {
  switch (operator) {
    case "+":
      return a + b;
    case "-":
      return a - b;
    case "*":
      return a * b;
    case "/":
      return a / b;
    default:
      throw new Error("Invalid operator");
  }
};

계산기 함수가 매우 간단한 함수지만 예약어(else, case, break)들 때문에 많은 코드가 필요하다고 느껴진다.

이때 디스패치 테이블을 사용하면, 이런 예약어를 줄이고 가독성을 높일 수 있다.

const CALCULATE_DISPATH_TABLE = {
  "+": (a: number, b: number) => a + b,
  "-": (a: number, b: number) => a - b,
  "*": (a: number, b: number) => a * b,
  "/": (a: number, b: number) => a / b,
};
 
type Operater = keyof typeof CALCULATE_DISPATH_TABLE;
 
const calculate = (operator: Operater, a: number, b: number) => CALCULATE_DISPATH_TABLE[operator](a, b);

내가 생각하는 디스패치테이블이 else if, switch 보다 좋다고 생각하는 이유는 다음과 같다.

  • 조건이 명시적이다. 객체의 key값이 조건이므로 해당 코드가 해야하는 일을 명시적으로 나타낸다.
  • 테이블을 사용하는 함수 외부에 위치할 수 있다.
  • 타입을 위한 추가적인 코드를 작성하지 않아도 된다.

다만 TypeScript에서 디스패치 테이블을 사용하다보면 인자 타입을 맞춰주지 못하거나 추론할 때 타입에러를 많이 만들 수 있다.

이를 위해 createDispatch라는 함수를 만들었다.

type AnyFunction = (...args: any[]) => any;
 
type FunctionMap = {
  [key: string]: AnyFunction;
};
 
function createDispatch<T extends FunctionMap>(obj: T) {
  const execute = <K extends keyof T>(key: K, ...args: Parameters<T[K]>): ReturnType<T[K]> => {
    const func = obj[key];
    return func(...args);
  };
 
  const isKey = (key: any): key is keyof T => {
    return key in obj;
  };
 
  return [execute, isKey] as const;
}

createDispath 함수는 두 가지 함수를 담은 튜플을 반환한다.

  • execute : 디스패치 테이블 실행 함수이다. 첫번째 인자로 디스패치 테이블의 키값과 키에 해당하는 함수의 인자를 인자로 받는다.
  • isKey : 해당 값이 디스패치 테이블의 값인지 체크하고 이후 타입가드를 수행한다.

createDispath 함수를 사용하면 타입스크립트를 통해 타입 힌트도 받을 수 있고 isKey를 통해 타입 가드도 받을 수 있다.

const CALCULATE_DISPATH_TABLE = {
  "+": (a: number, b: number) => a + b,
  "-": (a: number, b: number) => a - b,
  "*": (a: number, b: number) => a * b,
  "/": (a: number, b: number) => a / b,
};
 
const [calculate, isOperator] = createDispatch(CALCULATE_DISPATH_TABLE);
 
const cal = (key: string, arg: { a: number; b: number }) => {
  if (isOperator(key)) {
    return calculate(key, arg.a, arg.b);
  }
};

dispatch.gif