TypeScript とは
TypeScript はマイクロソフトによって開発され、メンテナンスされているフリーでオープンソースのプログラミング言語です。
TypeScript は JavaScript に対して、省略も可能な静的型付けとクラスベースオブジェクト指向が加わりました。
詳しくは公式サイトをどうぞ。
導入
create-nuxt-app で TypeScript を選択
% npx create-nuxt-app nuxt-typescript
create-nuxt-app v3.6.0
✨ Generating Nuxt.js project in nuxt-typescript
? Project name: nuxt-typescript
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? What is your GitHub username? XXXXX
? Version control system: Git
モジュールを追加
$ npm install --save-dev @nuxt/types
ファイルの作成
$ touch shims-vue.d.ts
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
tsconfig.jsonに追記
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"]
},
"types": ["@nuxt/types", "@types/node"]
},
"files": ["shims-vue.d.ts"],
"include": [
"components/**/*.ts",
"components/**/*.vue",
"layouts/**/*.ts",
"layouts/**/*.vue",
"pages/**/*.ts",
"pages/**/*.vue"
],
"exclude": ["node_modules", ".nuxt", "dist"]
}
これで完了です。
Composition API とは
v2.x 系の Options API での課題は以下の通りです。
コードがコンポーネントのthis
に依存するため、
コンポーネントのオプション(data
、computed
、methods
、watch
)にコードが分割され、
可読性が著しく落ちることでした。
皆さんも公式サイトの画像のように、このように分断されてしまうことが多いのではないでしょうか?
この問題を解決するために、生まれたのが Composition API です。
Vue/composition-apiの導入
モジュールを追加
% npm install --save @vue/composition-api
プラグインの追加
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
nuxt.config.js に追加
export default {
plugins: ['@/plugins/composition-api'],
};
Nuxt/composition-apiの導入
モジュールを追加
% npm install @nuxtjs/composition-api --save
nuxt.config.js に追加
buildModules: [
'@nuxtjs/composition-api/module'
]
nuxt.config.js に追加
generate: {
interval: 2000,
}
これで完了です。
新要素
【TypeScript の型推論のために】defineComponent
v2.x のときは、Vue.extend
が必要でした。
Composition API では、defineComponent に変更されました。
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({});
</script>
【v2 のオプションをひとまとめに!】setup コンポーネント(引数: props, context)
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
export default defineComponent({
props: {
times: {
type: Number,
default: 2,
},
},
setup(props, context) {
// 2を取得
const times = computed(() => {
return props.times;
});
// Attributes (Non-reactive object)
console.log(context.attrs);
// Slots (Non-reactive object)
console.log(context.slots);
// Emit Events (Method)
console.log(context.emit);
},
});
</script>
【data はリアクティブに】ref, reactive
<template>
<div>
{{ count }}
{{ task }}
{{ status }}
{{ tasks }}
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs } from '@vue/composition-api';
type TaskType = {
task: string;
status: string;
tasks: string[];
};
export default defineComponent({
setup() {
// ref
const count = ref<number>(0);
console.log(count); // { value: 0 }
console.log(count.value) // 0
count.value ++;
console.log(count.value) // 1
// reactive
const state = reactive<TaskType>({
task: '',
status: '',
tasks: []
})
return {
count,
// このようにスプレッド構文で取得するとスマート(ただこの場合、toRefsがないとリアクティブじゃなくなる)
...toRefs(state);
}
},
});
</script>
リアクティブとはなんぞや
何かの値が変更された時、依存する値も変わって欲しいというのがリアクティブです。
// リアクティブじゃない
let x = 1;
let y = x + 4; // 5 → 5
x = 3;
// リアクティブ
let x = 1;
let y = x + 4; // 5 → 7
x = 3;
このリアクティブを実現するために、refとreactiveがあります。
使い分けに関して
ref | reactive |
---|---|
プリミティブ型 | オブジェクト型 |
ちなみに、プリミティブとは 5 つの型を指します。
・number
・string
・boolean
・null
・undefined
methods もただの関数になりました
v2.X での書き方はこのような感じです。
<script>
export default {
data() {
return {
tasks: [],
task: '',
status: '作業中',
};
},
methods: {
addTask() {
this.tasks.push({
name: this.task,
status: this.status,
});
},
},
};
</script>
これが Composition API ではこのように変わりました。
<template>
<div>
<ul>
<li v-for="(task, index) in tasks" :key="index">
<span v-if="task.status === true"> {{ index }} : {{ task.name }} / 作業中 </span>
<span v-else> {{ index }} : {{ task.name }} / 完了 </span>
</li>
</ul>
<input type="text" v-model="task" />
<button @click="addTask">+</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
type Tasks = {
name: string
status: boolean
};
export default defineComponent({
setup() {
const task = ref<string>('');
const status = ref<boolean>(true);
const tasks = ref<Tasks[]>([]);
const addTask = () => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
return {
task,
status,
tasks,
addTask,
};
},
});
</script>
computed は、@vue/composition-api から持ってくる
<template>
<div>
<h2>Task: {{ doTasks }}</h2>
<ul>
<li v-for="(task, index) in tasks" :key="index">
<span v-if="task.status"> {{ index }} : {{ task.name }} / 作業中 </span>
<span v-else> {{ index }} : {{ task.name }} / 完了 </span>
</li>
</ul>
<input type="text" v-model="task" />
<button @click="addTask">+</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default defineComponent({
setup() {
const task = ref<string>('');
const status = ref<boolean>(true);
const tasks = ref<Tasks[]>([]);
const addTask = () => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const doTasks = computed(() => {
return tasks.value.filter(t => t.status).length;
});
return {
task,
status,
tasks,
addTask,
doTasks,
};
},
});
</script>
【ハンズオン】実際にTodoリストを作ってみよう!
新規追加
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<input type="radio" id="all" name="type" checked />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" />
<label for="complete">完了</label>
<br />
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody>
<tr v-for="(task, index) in tasks" :key="index">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button>作業中</button></td>
<td v-else><button>完了</button></td>
<td><button>削除</button></td>
</tr>
</tbody>
</table>
<br />
<h2>新規タスクの追加</h2>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default defineComponent({
setup() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
return {
// state
task,
status,
tasks,
// methods
addTask,
};
},
});
</script>
<style></style>
削除
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<input type="radio" id="all" name="type" checked />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" />
<label for="complete">完了</label>
<br />
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody>
<tr v-for="(task, index) in tasks" :key="index">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button>作業中</button></td>
<td v-else><button>完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
</tbody>
</table>
<br />
<h2>新規タスクの追加</h2>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default defineComponent({
setup() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const deleteTask = (id: number): void => {
tasks.value.splice(id, 1);
};
return {
// state
task,
status,
tasks,
// methods
addTask,
deleteTask,
};
},
});
</script>
<style></style>
タスク状態変更
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<input type="radio" id="all" name="type" checked />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" />
<label for="complete">完了</label>
<br />
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody>
<tr v-for="(task, index) in tasks" :key="index">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
</tbody>
</table>
<br />
<h2>新規タスクの追加</h2>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default defineComponent({
setup() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const deleteTask = (id: number): void => {
tasks.value.splice(id, 1);
};
const updateTask = (id: number): void => {
tasks.value[id].status = !tasks.value[id].status;
};
return {
// state
task,
status,
tasks,
// methods
addTask,
deleteTask,
updateTask,
};
},
});
</script>
<style></style>
タスク表示切り替え
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<input type="radio" id="all" name="type" value="すべて" v-model="radio" />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" value="作業中" v-model="radio" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" value="完了" v-model="radio" />
<label for="complete">完了</label>
<br />
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody v-for="(task, index) in tasks" :key="index">
<tr v-if="radio === 'すべて'">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '作業中' && task.status === true">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '完了' && task.status === false">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
</tbody>
</table>
<br />
<h2>新規タスクの追加</h2>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default defineComponent({
setup() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
const radio = ref<string>('すべて');
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const deleteTask = (id: number): void => {
tasks.value.splice(id, 1);
};
const updateTask = (id: number): void => {
tasks.value[id].status = !tasks.value[id].status;
};
return {
// state
task,
status,
tasks,
radio,
// methods
addTask,
deleteTask,
updateTask,
};
},
});
</script>
<style></style>
コードの問題点
コンポーネント内に、「Viewと状態管理とロジック」が混在していることです。
上記のToDoであれば、100行も満たないコードなので問題ないですが、
大規模なコードであれば可読性が下がったりメンテナンスしにくいなどの問題が発生します。
(ちなみに、上記のようなToDoの場合、v2.xの方が書きやすいですw)
それでは、コンポーネントから、「View」と「状態管理+ロジック」に切り分けましょう!
コンポーネントから「状態管理+ロジック」を切り分ける
新たなフォルダを作り、そこに状態管理とロジックを定義しましょう。
mkdir composables
mkdir composables/store
touch composables/store/taskLogic.ts
これでフォルダとファイルの作成が完了しました。
それでは、こちらのファイル内に移していきましょう!
import { ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default function taskLogic() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
const radio = ref<string>('すべて');
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const deleteTask = (id: number): void => {
tasks.value.splice(id, 1);
};
const updateTask = (id: number): void => {
tasks.value[id].status = !tasks.value[id].status;
};
return {
// state
task,
status,
tasks,
radio,
// methods
addTask,
deleteTask,
updateTask,
};
}
コンポーネント側はimportして必要なものをreturnで返すだけです。
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<input type="radio" id="all" name="type" value="すべて" v-model="radio" />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" value="作業中" v-model="radio" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" value="完了" v-model="radio" />
<label for="complete">完了</label>
<br />
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody v-for="(task, index) in tasks" :key="index">
<tr v-if="radio === 'すべて'">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '作業中' && task.status === true">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '完了' && task.status === false">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
</tbody>
</table>
<br />
<h2>新規タスクの追加</h2>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
// logic
import taskLogic from '@/composables/store/taskLogic';
export default defineComponent({
setup() {
const { task, status, tasks, radio, addTask, deleteTask, updateTask } = taskLogic();
return {
// state
task,
status,
tasks,
radio,
// methods
addTask,
deleteTask,
updateTask,
};
},
});
</script>
<style></style>
Provide/Injectでコンポーネント内で「状態管理+ロジック」を共有
①composablesにフォルダとファイルを作成
$ mkdir composables/key
$ touch composables/key/taskLogicKey.ts
②composables/key/taskLogicKey.tsにキーを作成する
import { InjectionKey } from '@vue/composition-api';
import { taskLogicStore } from '../store/taskLogic';
const taskLogicKey: InjectionKey<taskLogicStore> = Symbol('TaskLogicStore');
export default taskLogicKey;
③composables/store/taskLogic.tsで型情報を追加する
import { ref } from '@vue/composition-api';
type Tasks = {
name: string;
status: boolean;
};
export default function taskLogic() {
// state
const task = ref<string>('');
const status = ref<boolean>(true); // true->作業中, false->完了
const tasks = ref<Tasks[]>([]);
const radio = ref<string>('すべて');
// methods
const addTask = (): void => {
tasks.value.push({
name: task.value,
status: status.value,
});
task.value = '';
};
const deleteTask = (id: number): void => {
tasks.value.splice(id, 1);
};
const updateTask = (id: number): void => {
tasks.value[id].status = !tasks.value[id].status;
};
return {
// state
task,
status,
tasks,
radio,
// methods
addTask,
deleteTask,
updateTask,
};
}
// 型情報を追加する
export type taskLogicStore = ReturnType<typeof taskLogic>;
④親コンポーネントでProvide(=子コンポーネントに運ぶ)
<template>
<div>
<h1>ToDoリスト</h1>
<br />
<Radio />
<br />
<Table />
<br />
<h2>新規タスクの追加</h2>
<Task />
</div>
</template>
<script lang="ts">
import { defineComponent, provide } from '@vue/composition-api';
// logic
import taskLogicStore from '@/composables/store/taskLogic';
// key
import taskLogicKey from '@/composables/key/taskLogicKey';
// components
import Radio from '@/components/Radio.vue';
import Table from '@/components/Table.vue';
import Task from '@/components/Task.vue';
export default defineComponent({
components: { Radio, Table, Task },
setup() {
provide(taskLogicKey, taskLogicStore());
},
});
</script>
<style></style>
⑤子コンポーネントでInject(=親コンポーネントから受け取る)
<template>
<div>
<input type="radio" id="all" name="type" value="すべて" v-model="radio" />
<label for="all">すべて</label>
<input type="radio" id="work" name="type" value="作業中" v-model="radio" />
<label for="work">作業中</label>
<input type="radio" id="complete" name="type" value="完了" v-model="radio" />
<label for="complete">完了</label>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/composition-api';
// logic
import taskLogicStore from '@/composables/store/taskLogic';
// key
import taskLogicKey from '@/composables/key/taskLogicKey';
export default defineComponent({
setup() {
const { radio } = inject(taskLogicKey) as taskLogicStore;
return {
// state
radio,
};
},
});
</script>
<style></style>
<template>
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>状態</th>
</tr>
</thead>
<tbody v-for="(task, index) in tasks" :key="index">
<tr v-if="radio === 'すべて'">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '作業中' && task.status === true">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
<tr v-else-if="radio === '完了' && task.status === false">
<td>{{ index }}</td>
<td>{{ task.name }}</td>
<td v-if="task.status"><button @click="updateTask(index)">作業中</button></td>
<td v-else><button @click="updateTask(index)">完了</button></td>
<td><button @click="deleteTask(index)">削除</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/composition-api';
// logic
import taskLogicStore from '@/composables/store/taskLogic';
// key
import taskLogicKey from '@/composables/key/taskLogicKey';
export default defineComponent({
setup() {
const { task, status, tasks, radio, deleteTask, updateTask } = inject(
taskLogicKey
) as taskLogicStore;
return {
// state
task,
status,
tasks,
radio,
// methods
deleteTask,
updateTask,
};
},
});
</script>
<style></style>
<template>
<div>
<input type="text" v-model="task" />
<button @click="addTask()">追加</button>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/composition-api';
// logic
import taskLogicStore from '@/composables/store/taskLogic';
// key
import taskLogicKey from '@/composables/key/taskLogicKey';
export default defineComponent({
setup() {
const { task, addTask } = inject(taskLogicKey) as taskLogicStore;
return {
// state
task,
// methods
addTask,
};
},
});
</script>
<style></style>
これで完了です!