Help us understand the problem. What is going on with this article?

Vue.js in TypeScript で型定義をどこに書けばいいか

この記事について

表題の通りですが、いくつかバリエーションあるよなぁと思ったので、自分の思考を整理するために文書化しました。

パターン

  1. API から取得するバックエンドと共通で使用するデータモデルの型
  2. フロントエンドでのみ使用する型(関連する関数を持つドメインモデル)
  3. コンポーネント内のみで使用する型
  4. 親子コンポーネント間で共有する型

TODO 管理を例にとって考えます。

1. API から取得するバックエンドと共通で使用するデータモデルの型

types/task.d.ts に書く。

コンポーネントやストア(Vuex)から API を呼び出した際に、戻ってくるオブジェクト用の型です。
JSON を object にしただけなので、メソッドはありません。
any で扱うよりは interface として扱ったほうがいい場合に使います。

export interface TaskData {
  id?: number;
  name?: string;
  state?: string; // enum でも
  priority?: string; // enum でも
}

使い方はこんなかんじ:

const response = await axios.get(url);
const task: TaskData = response.data;

2. フロントエンドでのみ使用する型(関連する関数を持つドメインモデル)

modules/task.ts に書く。

API から返ってくるデータもドメインモデルの型ですが、こちらは単なるデータモデルなので、オブジェクト指向的に扱おうとすると少々めんどくさいことになります(なので、個人的には関数型っぽくやるほうがフィットするとは思いますが、オブジェクト指向的にやろうとするとクラスを使いたくなります)。

modules は classes でも domain でも models でもなんでもいいですが、ドメインモデルに関するモジュールをここに入れます。

import { TaskData } from '@/types/task';

export default class Task {
  private data: TaskData;
  constructor(data: TaskData) {
    this.data = data;
  }
  get data(): TaskData {
    return this.data;
  }
  public close(): void {
    this.data.status = 'closed';
  }
}

以下のような関数型っぽい構成にしてもいいかもしれません。

import { TaskData } from '@/types/task';
export const closeTask = (task: TaskData): void => task.status = 'closed';

ちょっと例が単一のプロパティを更新するだけのやつなのでアレなんですが、もっと複雑なやつだと関数化する意味があるので、そういうのを想像していただければ。

使い方はこんなかんじ:

import Task from '@/modules/task';
// or
import { closeTask } from '@/modules/task';

export default Vue.extend({
  props: {
    task: {
      type: Object as Task,
      required: true,
    },
  },
  // 略
  methods: {
    // ボタンなどの「タスクを完了する」アクション用のイベントハンドラ
    onCloseTask(): void {
      this.task.close();
      // or
      closeTask(this.task);
    },
  },
}

3. コンポーネント内のみで使用する型

コンポーネントの中に書く。

特定の Vue コンポーネント内でのみ使用する型は、コンポーネント内で宣言します。

例えば、タスク一覧ページで絞り込みをするようなコンポーネントがあったとき、「すべて」「完了」「未完了」の3つで絞り込めるとしたときに、型を使って、

enum FilteringTaskStatus {
  All = 'all',
  Done = 'done',
  Undone = 'undone',
}

const filterTaskStatus = (task: Task, value: FilteringTaskStatus): boolean => {
  if (value === FilteringTaskStatus.All) {
    return true;
  }
  if (value === FilteringTaskStatus.Done) {
    return task.done();
  }
  if (value === FilteringTaskStatus.Undone) {
    return !task.done();
  }
  return false;
};

type FilterValue = FilteringTaskStatus; // 型が増えたら Union にする

interface TaskFilter {
  value: FilterValue;
  filter: (task: Task, value: FilterValue) => boolean;
}

interface TaskFilters {
  // ] のあとの : が隣接していると行ごと消えるので間に半角スペースを入れています
  [key: string] : TaskFilter;
}

使い方

export default Vue.extend({
  data() {
    return {
      filters: TaskFilters = {
        'status': {
          value: FilteringTaskStatus.All,
          filter: filterTaskStatus,
        },
      },
    }
  },
  computed: {
    filteredTasks(): Task[] {
      return this.tasks.filter((task: Task): boolean => {
        for (let key in this.filters) {
          const f = this.filters[key].filter;
          const v = this.filters[key].value;
          if (!f(task, v)) {
            return false;
          }
        }
        return true;
      });
    },
  },
}

4. 親子コンポーネント間で共有する型

子コンポーネントの中に書く。

props down / events up の方式で親子間でデータのやり取りをする場合、emit する側と handle する側で型を合わせておいたほうがいい局面があります(オブジェクトを返すようなとき)。

またしてもいい例が思い浮かばなかったんですが、タスク作成モーダルを子コンポーネントに持っているタスク一覧ページコンポーネントがあったとき、「続けて作成」チェックボックスをオンにするとモーダルを閉じずに次のタスクを作成できる、みたいな UI だったとき、作成したタスクオブジェクトと boolean の値を一緒に送ることになります。

子コンポーネント

export interface TaskCreateResult {
  task: Task;
  isContinued: boolean;
}

export default Vue.extend({
 // (略)
  emitCreate(value: TaskCreateResult): void {
    this.$emit('create', value);
  }
});

親コンポーネント

import TaskCreateModal, { TaskCreatedResult } from '@/components/TaskCreateModal';

export default Vue.extend({
  methods: {
    onOkClick(result: TaskCreatedResult) {
      if (!result.isContinued) {
        this.visibleCreateModal = false;
      }
    },
  },
});

おわりに

他にも、こういうケースではここに型定義を置くといいよ、みたいなのありましたら、コメント欄にて教えていただけると助かります :bow:

nunulk
PHP, Laravel, オブジェクト指向プログラミング, デザインパターン, リファクタリング, 関数プログラミング, etc.
http://nunulk.hatenablog.com
phper-oop
ペチオブはオブジェクト指向ワーキンググループです。様々なエンジニアの方に参加頂いております。
https://phper-oop.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした