🦁

Vue3 の Inject / Provide を TypeScript のクラスで管理する

2022/12/20に公開

はじめに

Vue3 で導入された Inject/Provide、便利ですよね。
TypeScript からは、以下のようなスタイルで定義するのがスタンダードかと思います。

従来の定義

import { readonly, ref, type InjectionKey } from "vue";

export const useTestData = () => {
  const textData = ref("");
  const privateNumber = ref(10);
  const publicNumber = readonly(privateNumber);

  const someMethod = () => {
    console.log("hello");
  };

  return {
    textData,
    publicNumber,
    someMethod,
  };
};

export type TestData = ReturnType<typeof useTestData>;
export const Key: InjectionKey<TestData> = Symbol("TestData");

従来の使用方法

import { useTestData, Key, type TestData } from "./test-data";

// 提供側
provide(Key, useTestData());

// 使用側
const testData = inject(Key) as TestData;
// あるいは
const { textData, publicNumber } = inject(Key) as TestData;

長らくこのスタイルで使っていたのですが、ある日同僚のK君から「こういう定義の方法もあるよ」と教えてもらった方法がとても便利だったので、許可をもらって記事する事にしました。
(K君ありがとう!!)

クラススタイルでの定義

従来のスタイルでは「関数が呼ばれるたびにスコープで閉じたプロパティとメソッドを持ったオブジェクトを返す」という挙動になっているのですが、これって実はクラスでパッケージしても同じなんですよね。
つまり、以下のようなクラス定義でも同じ事が実現可能です。

定義

import { readonly, ref, type InjectionKey } from "vue";

export class TestData {
  public static readonly Key: InjectionKey<TestData> = Symbol("TestData");

  public readonly textData = ref("");
  private readonly privateNumber = ref(10);

  public readonly publicNumber = readonly(this.privateNumber);

  public readonly someMethod = () => {
    console.log("hello");
  };
}

使用方法

import { TestData } from "./test-data";

// 提供側
provide(TestData.Key, new TestData());

// 使用側
const testData = inject(TestData.Key) as TestData;
// あるいは
const { textData, publicNumber } = inject(TestData.Key) as TestData;

嬉しいポイント

個人的に嬉しかったのは以下の点です。

コードが短くなる

単純に、同じ事を定義するためのコード量が少なくなっているのはとても効率的で良い事だと思います。
クラス化によってKeystatic で持つ事が可能になるので、使用する際の import 量も減っていますね。

型を Type-Only import しなくて良い

この例でいう所の TestDatatype ではなく class として使用されるので、 Type-Only import の必要が無くなります。

型定義や公開設定が不要

クラスになっているので、 public 宣言になっているものは公開、 private 宣言になっているものは非公開と、定義時点で明確な公開範囲設定がなされるので、別途どれを公開するか選ぶ必要が無くなります。
また、従来のスタイルでは型を別に定義する必要がありましたが、クラスが型の情報を持っているので不要になります。

個人的な感想ですが、TypeScript「一度定義したらその情報を上手く再利用してくれて、結果的に手間が省ける」 という運用方法はとても美しいと思っています。

readonly な実装にできる

こちらもクラスになっているので、readonly 宣言を使う事が出来ます。
従来のスタイルだと

const testData = inject(TestData.Key) as TestData;
testData.textData = ref("");
testData.someMethod = () => {};

というように型さえ合っていればその値に再代入できてしまいますが、クラススタイルでは readonly を使用できるので、代入しようとしてもエラーが出ます。

読み取り専用プロパティであるため、'textData' に代入することはできません。

こんな用途を想定する必要が無ければ、各 readonly 宣言を外すだけで従来スタイルと同じ定義になるので、さらにコードを短くする事が出来ます。

注意点

少しだけ注意点もあります。

thisが必要

クラスになるので、クラス内のスコープにアクセスするためには this が必要になります。
何かの記事で「Vue3CompositionAPIthis が不要になって綺麗になった!」というのを見た事があって、その流れには反する事になるなと思っています。

ただ、個人的にはスコープも明確になるので、そういう物だと思っていれば特に苦にならないなという印象です。
(それよりもメリットが大きいと思っている)

関数はラムダ式で定義する必要がある

サンプルをよく見ると、関数がラムダ式になっている事に気付くと思います。

  const someMethod = () => {
    console.log("hello");
  };

これは、ラムダ式で定義しないとthisのスコープが外れてしまうからです。

例えばこれを以下のように変更して

  const someMethod1 = () => {
    console.log(this);
  };
  public someMethod2() {
    console.log(this);
  }

呼び出すと、それぞれ以下のような結果になります。

▶ TestData {textData: RefImpl, privateNumber: RefImpl, publicNumber: Proxy, someMethod1: ƒ}
undefined

おわりに

個人的には、2022年一番の目からうろこ案件でした。
言われてみれば確かに!と思うのですが、中々そこに目が向かなかったというか。

みなさんも、心の中でK君に感謝しながら、ぜひこの方法を使ってみてください🙇

Discussion