🍌

google.scripts.run における引数の制約

2023/11/17に公開

code.gs
function myFunction(param) {
  console.log(param);

  return {
    ok: true,
    value: {
      fruit: "banana",
      count: 10,
    },
    date: new Date(),
  }
}
index.html
<script>
  function onSuccess(result) {
    console.log(result);
    // > null
  }

  const param = {
    ok: true,
    value: {
      fruit: "banana",
      count: 10,
    },
    // func: () => console.log("ok"),
    //       ^^^^^^^^^^^^^^^^^^^^^^^
    // Uncaught TypeError: Failed due to illegal value in property: func
  }

  google.script.run
    .withSuccessHandler(onSuccess)
    .myFunction(param)
</script>

なぜこのようなことが起こるか

クライアント <-> サーバーの通信では、関数の他、Date オブジェクトなどを渡せない。
https://developers.google.com/apps-script/guides/html/reference/run

「クライアント -> サーバー」では、Uncaught TypeError: Failed due to illegal value in property: {property name} が発生する。

「サーバー -> クライアント」では、成功判定の上で渡される値が null になる。

暫定的な解決策

以下では clasp を用いてローカルに開発環境を移し、TypeScript x React で開発をし、ライブラリ gas-client を用いる前提とする(勝手に飛躍してすみません)。
https://zenn.dev/hasehiro0828/articles/3eb9cb46527e02

result.ts
result.ts
type Result<T, E extends Error = Error> = Success<T> | Failure<E>;

class Success<T> {
  readonly value: T;

  constructor(value: T) {
    this.value = value;
  }
  isOk(): this is Success<T> {
    return true;
  }
  isErr(): this is Failure<never> {
    return false;
  }
}

class Failure<E extends Error> {
  readonly error: E;

  constructor(error: E) {
    this.error = error;
  }
  isOk(): this is Success<unknown> {
    return false;
  }
  isErr(): this is Failure<E> {
    return true;
  }
}

function Ok<T>(value: T) {
  return new Success(value);
}

function Err<E extends Error>(err: E) {
  return new Failure(err);
}
サーバー側の適当な関数
type MyType = {
  ok: boolean;
  value: {
    fruit: string;
    count: number;
    nested: {
      hoge: boolean;
      fuga: (number | number[])[];
    };
  };
};

function myFunction(param: MyType, param2: string): Result<MyType> {
  console.log(param, param2);

  return Ok({
    ok: true,
    value: {
      fruit: "banana",
      count: 10,
      nested: {
        hoge: true,
        fuga: [0.1, 2, [3, 4]],
      },
    },
  });
}
gas-client に api として認識させる部分
type ApiData =
  | string
  | number
  | boolean
  | undefined
  | null
  | { [key: number]: ApiData }
  | { [key: string]: ApiData }
  | ApiData[]
  | HTMLFormElement;

type ApiResult<T extends ApiData> =
  | {
      ok: true;
      data: T;
    }
  | {
      ok: false;
      name: string;
      message: string;
    };

function apiHandler<T extends ApiData>(
  proc: () => Result<T>
): ApiResult<T> {
  const result = proc();

  if (result.isOk()) {
    return {
      ok: true,
      data: result.value,
    };
  }

  return {
    ok: false,
    name: result.error.name,
    message: result.error.message,
  };
}

export function apiMyFunction(...p: Parameters<typeof myFunction>) {
  return apiHandler(() => myFunction(...p));
}
クライアント側
import { GASClient } from "gas-client";
import * as server from "../server/main";
const { serverFunctions } = new GASClient<typeof server>();

function App() {
  const param = {
    ok: true,
    value: {
      fruit: "banana",
      count: 10,
      nested: {
        hoge: true,
        fuga: [0.1, 2, [3, 4]],
      },
    },
  };

  serverFunctions.apiMyFunction(param, "param2").then((v) => {
    console.log(v);
  });

  return <></>;
}

export default App;

解決したこと

  • サーバーからの返り値を制限できた

解決してないこと

  • クライアントから渡す引数の型を制限できていない
    • GAS の制限により export const が出来ないため
    • 任意の引数の型を受け取る関数が作成できないため

良い方法あったら教えてください。

GitHubで編集を提案

Discussion