見出し画像

関数のオーバーロードをTypescriptでやってみた~SQLにおけるunnest関数の実装を通して~

こんにちわ。nap5です。

SQLにおけるunnest関数の実装を通して関数のオーバーロードをTypescriptでやってみましたので、紹介です。

関数のオーバーロードについては以下にまとまっています。
inferで頑張るのがしんどくなってきたときには重宝しそうです。
また、アロー関数にはオーバーロードの構文がデフォではないので、いわゆる関数で実現していくのが、よさそうです。


unnestのイメージは以下を参考にしました。


定義側です


import { ArrayPath } from "dot-path-value";
import { omit } from "radash";
import type { Simplify } from "type-fest";

type ArrayElement<T> = T extends (infer U)[] ? U : never;

// オーバーロードのシグネチャ1: alias が指定された場合
export function unnest<T extends Record<string, unknown>, K extends ArrayPath<T> | keyof T, A extends string>(
  data: T[],
  arrayKey: K,
  alias: A
): Simplify<Omit<T, K> & { [P in A]: ArrayElement<T[K]> }>[];

// オーバーロードのシグネチャ2: alias が指定されない場合
export function unnest<T extends Record<string, unknown>, K extends ArrayPath<T> | keyof T>(
  data: T[],
  arrayKey: K
): Simplify<Omit<T, K> & { [P in K]: ArrayElement<T[K]> }>[];

// 関数の実装
export function unnest<T extends Record<string, unknown>, K extends ArrayPath<T> | keyof T, A extends string>(
  data: T[],
  arrayKey: K,
  alias?: A
) {
  const result: (Omit<T, K> & { [key: string]: ArrayElement<T[K]> })[] = [];

  data.forEach((item) => {
    const arrayData = item[arrayKey] as Array<ArrayElement<T[K]>>;
    arrayData.forEach((subItem) => {
      const clonedItem: Omit<T, K> & { [key: string]: ArrayElement<T[K]> } = {
        ...omit(item, [arrayKey]),
        [alias ?? arrayKey]: subItem,
      };
      result.push(clonedItem);
    });
  });

  return result;
};

// 関数オーバーロードを使用すると、関数の実装部分で any を使用していても、オーバーロードのシグネチャで指定された型情報が呼び出し側に適用される
export function unstable_unnest<T extends Record<string, unknown>, K extends ArrayPath<T>, A extends string>(
  data: T[],
  arrayKey: K,
  alias?: A
): any[] {
  const result: any[] = [];

  data.forEach((item) => {
    const arrayData = item[arrayKey] as Record<string, unknown>[];
    arrayData.forEach((subItem) => {
      const clonedItem = {
        ...omit(item, [arrayKey]),
        [alias ?? arrayKey]: subItem,
      };
      result.push(clonedItem);
    });
  });

  return result;
};


使用側です

import { describe, test, expect } from "vitest";
import { unnest } from "@/index";

describe("unnest", () => {
  test("Some data", () => {
    type Person = {
      id: number;
      name: string;
      roles: ("owner" | "member" | "admin")[];
    };

    const data: Person[] = [
      {
        id: 1,
        name: "John",
        roles: [
          "admin",
          "owner"
        ],
      },
      {
        id: 2,
        name: "Jane",
        roles: [
          "admin",
          "owner",
          "member"
        ],
      },
    ];
    const result = unnest(data, "roles", "role");
    expect(result).toStrictEqual([
      { id: 1, name: "John", role: "admin" },
      { id: 1, name: "John", role: "owner" },
      { id: 2, name: "Jane", role: "admin" },
      { id: 2, name: "Jane", role: "owner" },
      { id: 2, name: "Jane", role: "member" },
    ]);
  });


  test("Person data", () => {
    type Person = {
      id: number;
      name: string;
      hobbies: { id: number; name: string }[];
    };

    const data: Person[] = [
      {
        id: 1,
        name: "John",
        hobbies: [
          { id: 1, name: "reading" },
          { id: 2, name: "gaming" },
        ],
      },
      {
        id: 2,
        name: "Jane",
        hobbies: [
          { id: 3, name: "hiking" },
          { id: 4, name: "cooking" },
          { id: 5, name: "swimming" },
        ],
      },
    ];
    const result = unnest(data, "hobbies", "hobby");
    expect(result).toStrictEqual([
      { id: 1, name: "John", hobby: { id: 1, name: "reading" } },
      { id: 1, name: "John", hobby: { id: 2, name: "gaming" } },
      { id: 2, name: "Jane", hobby: { id: 3, name: "hiking" } },
      { id: 2, name: "Jane", hobby: { id: 4, name: "cooking" } },
      { id: 2, name: "Jane", hobby: { id: 5, name: "swimming" } },
    ]);
  });

  test("Classroom data", () => {
    type Student = {
      id: number;
      name: string;
    };

    type Classroom = {
      id: number;
      subject: string;
      students: Student[];
    };

    const data: Classroom[] = [
      {
        id: 1,
        subject: "Mathematics",
        students: [
          { id: 1, name: "Alice" },
          { id: 2, name: "Bob" },
        ],
      },
      {
        id: 2,
        subject: "History",
        students: [
          { id: 3, name: "Charlie" },
          { id: 4, name: "Daisy" },
          { id: 5, name: "Edward" },
        ],
      },
    ];

    const result = unnest(data, "students", "student");
    expect(result).toStrictEqual([
      { id: 1, subject: "Mathematics", student: { id: 1, name: "Alice" } },
      { id: 1, subject: "Mathematics", student: { id: 2, name: "Bob" } },
      { id: 2, subject: "History", student: { id: 3, name: "Charlie" } },
      { id: 2, subject: "History", student: { id: 4, name: "Daisy" } },
      { id: 2, subject: "History", student: { id: 5, name: "Edward" } },
    ]);
  });


  test("Classroom data non alias", () => {
    type Student = {
      id: number;
      name: string;
    };

    type Classroom = {
      id: number;
      subject: string;
      students: Student[];
    };

    const data: Classroom[] = [
      {
        id: 1,
        subject: "Mathematics",
        students: [
          { id: 1, name: "Alice" },
          { id: 2, name: "Bob" },
        ],
      },
      {
        id: 2,
        subject: "History",
        students: [
          { id: 3, name: "Charlie" },
          { id: 4, name: "Daisy" },
          { id: 5, name: "Edward" },
        ],
      },
    ];

    const result = unnest(data, "students");
    expect(result).toStrictEqual([
      { id: 1, subject: "Mathematics", students: { id: 1, name: "Alice" } },
      { id: 1, subject: "Mathematics", students: { id: 2, name: "Bob" } },
      { id: 2, subject: "History", students: { id: 3, name: "Charlie" } },
      { id: 2, subject: "History", students: { id: 4, name: "Daisy" } },
      { id: 2, subject: "History", students: { id: 5, name: "Edward" } },
    ]);
  });
});



demo code.



簡単ですが、以上です。

いいなと思ったら応援しよう!