TypeScript, Vue, Nuxt の Tips

1 年ほど前から、既存のフロントエンドアプリケーションを Vue.js 2 + Composition API + TypeScript に書き換える作業に携わっている。その作業の過程において、特に TypeScript に関してぶつかった問題に対する解法をメモしておいた。今回は、そのメモを元に今回の移植で習得した技術の断片、とくに Tips 的な「すぐに役立つ」知識をまとめてみた。

前提 :

- 使用しているフロントエンドフレームワークは Vue.js 2 である。
- Composition API を使用して実装しているので、実装内容は Vue.js 3 的である。
- Vue フレームワークとして Nuxt.js 2 を使用している。
- 実装言語は TypeScript である。
- スタイルはオブジェクト指向ではなく、関数型プログラミングである。

## `== null` と `!= null`

JavaScript (TypeScript) では等価性の比較に `==`, `!=` を用いるのはバッドプラクティスとされているが、一つ例外がある。

JavaScript (TypeScript) では、知らない人も多いと思われるが、`x == null` は `x === null || x === undefined` と等価であり、 `x != null` は `x !== null && x !== undefined` と等価である。

JavaScript (TypeScript) でオプショナルな値を扱う場合には、`T | null` もしくは `T | undefined` さらには `T | null | undefined` のいずれかを用いるが、どの場合でも `x` が `T` を持つことの確認に `!= null` を用いることができる。

```typescript
const f = (value?: string) => { // value: string | undefined
  if (value != null) {
    // Type narrowing: `value` is `string`, not `undefined` here
    const upper = value.toUpperCase()
  }
}

const g = (value: string | null) => {
  if (value != null) {
    // Type narrowing: `value` is `string`, not `null` here
    const upper = value.toUpperCase()
  }
}
```

利点

- 表記が簡潔。
- `T | null` なのか `T | undefined` なのか `T | null | undefined` なのか気にしないで `if (value != null)` によって type narrowing できる。

欠点

- 知らない人は `=== null`, `!== null` と表記が違っていることすら気付かないだろう。
- 気付いても意味が分かる人が少なく、意図が伝わりづらい。

## オブジェクトから特定のプロパティを取り去る方法

いま、

```typescript
const foos = { foo1: 1, foo2: 2, foo3: 3, foo4: 4, foo5: 5 }
```

があったとして、これから `foo2:`, `foo4:` だけを取り除いたオブジェクトがほしいとする。

以下のようにすれば良い :

```typescript
const { foo2, foo4, ...oddfoos} = foos
console.log(oddfoos)  // → Object { foo1: 1, foo3: 3, foo5: 5 }
```

なお、TypeScript では型からプロパティの型を取り除くことも可能で、`Omit<>` を用いて次のようにする :

```typescript
type Foo = {
  foo1: number
  foo2: number
  foo3: number
  foo4: number
  foo5: number
}
type OddFoo = Omit<Foo, 'foo2' | 'foo4'>
```

## props のリアクティブ性

```typescript
export default defineComponent({
  props: {
    value: { type: String, default: '' },
    count: { type: Number, default: 0 },
  },
  setup(props) {
    // Want to use `value` as ref:
    const value1 = computed(() => props.value)  // Good; `value1` is reactive
    const value2 = toRef(props, 'value')  // Good
    const upper = computed(() => props.value.toUpperCase())  // Good

    // Want to watch `value`:
    watch(props.value, (v) => { /* ... */ })  // Bad; `props.value` is not reactive
    watch(props, (p) => { /* ... */ })  // Not so good; It watches `value` and `count`.
    watch(() => props.value, (v) => { /* ... */ })  // Good
    watchEffect(() => { /* use props.value here */ })  // Good
  },
})
```

Vue コンポーネントの `props: ` で宣言したコンポーネントの属性は `setup(props)` の第 1 引数 `props` で受け取ることができる。この `props` はリアクティブオブジェクトであるが、`props` のプロパティ 1 つ 1 つはリアクティブではないことに注意する必要がある。

属性を個別にリアクティブ値として扱いたい場合は以下のようにする。

- 属性を個別のリアクティブ変数にしたい場合は、`computed()` もしくは `toRef()` を用いる。
- 個別の属性の変更を監視したい場合は `watch(() => props.value, ...)` を用いる。もしくは `watchEffect()` を用いる。

## `void 0` と `undefined`

`void 0` という JavaScript の表現を知っているだろうか？

```typescript
const x = {
  foo: void 0,
}
```

のように定数として使い、意味は `undefined` である。

```typescript
const x = {
  foo: undefined,
}
```

`undefined` は `window` (globalThis) のプロパティで、値 `undefined` を初期値として持つ。JavaScript の予約語とかではなく、普通の識別子である。

その昔、`window.undefined` は書き換え可能だったので、事故を避けるために `undefined` の代わりに `void 0` が使われた。

現在では `window.undefined` は書き換え不可なプロパティとして定義されているので、もはや `void 0` を使う必要はないし、使ってはならない（ぱっと見意味がよくわからないから）。

## 分割代入でネストしたオブジェクトのプロパティとネスト元を同時に代入する方法

Composition API や Nuxt のプラグインでインジェクトされたコンテキストを利用していると、JavaScript (TypeScript) の分割代入を使う機会が増える。分轄代入機能ではネストしたオブジェクトのプロパティの代入を行うことができる。

```typescript
type DialogService = {
  $dialog: {
    $confirm: ConfirmDialogService
    $alert: AlertDialogService
  }
}

const useDialog = (): DialogService => { /* ... */ }

// ...

export default defineComponent({
  setup(props) {
    const { $dialog: { $confirm } } = useDialog()
    $confirm.show('hello')
  }
}
```

滅多にないことだが、上の例で `$dialog` も同時に取得したいときにはどのようにすればいいだろうか？そもそも可能なのか？

以下のようにできる :

```typescript
export default defineComponent({
  setup(props) {
    const { $dialog, $dialog: { $confirm } } = useDialog()
    $confirm.show('hello')
    $dialog.$alert('error')
  }
}
```

逆に言うと、`const { $dialog: { $confirm } } = useDialog()` では、`$dialog` は変数にはならない。

## 厳密なオブジェクトリテラルチェック

TypeScript ではサブタイプ（拡張した型）の値をスーパータイプに割り当てることができる（普通のオブジェクト指向言語と同じ）。

```typescript
type A = { a: number }
type B = A & { b: string }

const f = (a: A, b: B) => {
  a = b  // Good; B は A を拡張した型（サブタイプ）だから; b = a はエラー
}
```

ところが、型 B の値である `{ a: 1, b: 'b' }` というリテラルを直接代入しようとするとエラーになる。

```typescript
const f = (a: A) => {
  a = { a: 1, b: 'b' }  // ERROR
}
```

これは、TypeScript がリテラル値を代入する際に特別に厳しい型チェックをすることが原因で、リテラルの書き間違いを検出するためにこのようになっている。

一度変数を経由すればエラーを避けることができる。

```typescript
const f = (a: A) => {
  const c = { a: 1, b: 'b' }
  a = c  // Good
}
```

## DOM の配列を JavaScript (TypeScript) の配列として使う方法

DOM の API で得られる配列みたいなものは、配列のように扱えるが JavaScript の配列そのものではないことが多い（`HTMLCollection`）。

例えば以下のようなことはできない：

```typescript
const children = document.getElementById('foo').children
children.forEach((child) => { /* ... */ })  // ERROR
```

解決法 &mdash; `Array.from()` を使う：

```typescript
const children = Array.from(document.getElementById('foo').children)
children.forEach((child) => { /* ... */ })  // OK
```

ただし、`HTMLCollection` は **live** であることが特徴で、第 1 の例の `children` をずっと保持しておいて、その間に `foo` 要素の子孫が変化した場合、`chidren` 配列も更新される。一方、第 2 の例の `children` は、その時点の子孫の配列のコピーであり、変化しない。

## 条件付きスプレッド演算子

ある条件が満たされたときだけプロパティの値をセットしたい場合：

```typescript
type Item = {
  a?: number
}

const f = (b: boolean): Item => {
  return {
     ...(b ? { a: 1 } : {}),
  }
}

f(false)  // => {}
f(true)   // => { a: 1 }
```

と書けるだろう。より見通しの良い次の書き方が可能である：

```typescript
const f = (b: boolean): Item => {
  return {
     ...(b && { a: 1 }),
  }
}
```

（上の方が紛れがない書き方のような気がしないでもない...）

## オブジェクトのオプショナルプロパティと reactive

オプショナルなプロパティ `x?: type` を持つオブジェクトを `reactive()` でリアクティブ化する場合、`reactive()` に渡す値に `x` を含める必要がある。

仮に初期値が `undefined` なら `x: undefined` と明示的に指定する必要がある。

もし含めずに、後ほど `.x = xxx` と代入してもリアクティブの連鎖は発生しない。

```typescript
type T = {
  a: number
  b?: string
}

const state = reactive<T>({
  a: 5,
})

watch(state, (cur) =>{
  console.log(cur)
})

setTimetout(()=> {
  state.b = 'hello'  // → 代入しても watch() 内の console.log() は実行されない。
}, 1000)
```

正しくは以下のようにすべき：

```typescript
const state = reactive<T>({
  a: 5,
  b: undefined,
})
```

## typo に起因する非常に見つけにくいバグ

あるモジュールをデバッグしていて、間違った型を指定したにもかかわらず、それをインポートする特定の .vue ファイルだけでは、全く型エラーも発生しないし、Vetur によるエラーも出ないという謎現象に遭遇した。

ビルドは通るのだが、型を間違っているせいで実行はうまくいかない。その謎の .vue ファイルの一部を取り出してみた。

```vue
...
</template>

<script lant="ts">
import computed, defineComponent } from '@nuxtjs/composition-api'
...
```

気が付いただろうか？

`<script lant="ts">` ... lan<b>t</b> !!!

正しくは `<script lang="ts">` 。

教訓：手打ちはなるべく避けて、エディターで補完される箇所はエディターにまかせる

## non-null assertion を使わざるを得ないケース

type narrowing は関数境界をまたがないので、non-null assertion `!` を使わざるを得ないケースがある。

```typescript
const f = (list: readonly string[], item: ComputedRef<Item | null>) => {
  if (item.value != null) {
    const found = list.find((x) => x === item.value!.name) // ! がないと ERROR
  }
}
```

一応以下のようにすれば避けられるが、、、冗長。

```typescript
const f = (list: readonly string[], item: ComputedRef<Item | null>) => {
  if (item.value != null) {
    const name = item.value.name
    const found = list.find((x) => x === name)
  }
}
```

## `T | null` な props を宣言する方法

コンポーネント `<component :item="item">` で `item` を `Item | null` としたいとする。普通に考えれば

```typescript
export default defineComponent({
  props: {
    item: { type: Object as PropType<Item | null>, required: true },
  }
```

とするだろう。しかしこれは（Vue の「バグ」のせいで）うまくいかない。 `<component :item="null">` としたときに `[Vue warn]: Invalid prop: type check failed for prop "item". Expected Object, got Null` という警告が console に出てしまう。

現在のところ、対処法としては以下のようにするしかない。

```typescript
export default defineComponent({
  props: {
    item: { type: Object as PropType<Item | null>, default: null },
  }
```

`required: true` を指定できないので、 `<component>` のように `item` を指定しなかった場合の警告は出ないが、仕方がない。

## `is` の使いみちの例

`is` を用いてユーザー定義型ガードを作成できる。ユーザー定義型ガードによって、type narrowing を生の条件文ではなく関数で行うことができる。例えば：

```typescript
const isListElement(x: Element) x is HTMLLIElement => {
  return x instanceof HTMLLIElement
}

// ...

if (isListElement(e)) {
  // e は HTMLLIElement 型
  console.log(e.type)  // "disc", "square", or "circle"
}
```

上の例は１行の条件文を関数にしただけでありがたみが無いと思うかもしれないが、意外な使いみちがある。

配列から条件に適合した要素を抜き出す `.filter()` メソッドは `: is xxx` が返り値の条件関数を引数に取る場合、`xxx` 型の配列を返すように定義されている。

例えば `<ul>` の子供要素から `<li>` の配列を取得することを考えよう。

```vue
<template>
  <div>
    <ul ref="ul">
      <li><!-- ... --></li>
      <li><!-- ... --></li>
      <li><!-- ... --></li>
    </ul>
  </div>
</template>

<script lang="ts">
setup() {
  const ul = ref<Element>()

  watch(ul, (cur) => {
    if (cur) {
      const listitems = Array.from(cur.children).filter(
        (x) => x instanceof HTMLLIElement
      )
      listitems.forEach((li) => console.log(li.type))
        // ERROR: Property 'type' does not exist on type 'Element'.
    }
  })

  return { ul }
}
</script>
```

ではダメである。`listitems` が `Element[]` となってしまうからである。そこで `is` を用いると問題が解決する。

```typescript
setup() {
  const ul = ref<Element>()

  watch(ul, (cur) => {
    if (cur) {
      const listitems = Array.from(cur.children).filter(
        (x): x is HTMLLIElement => x instanceof HTMLLIElement
      )
      // listitems `is` HTMLLIElement[]
      listitems.forEach((li) => console.log(li.type))
    }
  })

  return { ul }
}
```

（なお、`is` も `as` と同様にしょせん強制型指定であるので、よりわかりやすい

```typescript
const listitems = Array.from(cur.children).filter(
  (x) => x instanceof HTMLLIElement
) as HTMLLIElement[]
```

の方が良いという説もある。。。）

## reactive の個別プロパティを watch で監視する

`reactive()` で生成したオブジェクトのプロパティを個別で監視したい場合がある。

```typescript
const dialog = reactive({
  a: '',
  b: 0,
})
```

`dialog.a` を監視したい場合:

`watch(dialog.a, ...)` はダメである。`dialog.a` はリアクティブではない。

`watch(dialog, ...)` で可能だが、`b: ` だけが変更されても監視関数が実行されてしまう。

正解は `watch(() => dialog.a, ...)` のように、監視対象を関数にすることである。

応用として、コンポーネントのプロパティを個別に監視したい場合がある。

```typescript
props: { p: { type: string, required: true } },
setup(props) {
  watch(() => props.p, ...
```

## Ref が変化するのはいつか？～コンピューター言語における等価性～

`ref()` で定義した `Ref` 型の変数は例えば `watch()` で変化を検出できる。

```typescript
const v = ref(0)
watch(v, (cur) => console.log(cur))

v.value = 0  // 何も起こらない
v.value = 1  // console に 1 が出る
```

`ref()` の実装では `.value` は `set()` （JavaScript 言語のセッター機能）により実装されており、 `.value =` という代入時には関数が実行される。

この関数で、現在 `.value` に入っている値と、これから代入しようとしている値を**比較**して「異なる」値を検出すると `watch()` のコールバックが呼ばれることになる。

ではこの**比較**とは実際には何なのだろうか？次の場合に `watch()` のコールバックが呼ばれるかどうか考えてみてほしい。

```typescript
const v = ref({ count: 0 })
watch(v, (cur) => console.log(cur.count))

v.value = { count: 0 }  // ???
v.value = { count: 1 }  // ???
```

正解は「どちらもコールバックが呼ばれる」である。

人間の目で見ると、前者（ `{ count: 0 }` から `{ count: 0 }` ）は値が変わっていないと認識されるので、この結果は奇妙に見える。この奇妙さは、コンピューター言語における等価性（比較）の実装から来るものである。

`ref()` の実装では等価性の比較に JavaScript 言語の `===` を用いている。 `===` は、左右が値オブジェクト（ `boolean`, `number`, `string` ）のときは、その値を比較し、そうでない場合は、言語処理系における内部のオブジェクトのアドレスを比較している。

このため `ref()` に渡した `{ count: 0 }` というオブジェクトと、後で代入した `{ count: 0 }` は（中身は同じだが）別々の場所で作られた別々のオブジェクトであって、内部アドレスが異なるので `===` は `false` を返す。よって等価でないと判断され `watch()` のコールバックが呼ばれるのである。

JavaScript 言語が用意する等価性機能は、基本的には `===` しかない。このため `{ count: 0 }` と `{ count: 0 }` を「同じ」だと判断するような機能を用意するためには自分で実装する必要がある。

例えば Jest ではこの機能を用意している。

```typescript
expect({ count: 0 }).toBe({ count: 0 })     // FAIL
expect({ count: 0 }).toEqual({ count: 0 })  // PASS
```

`.toBe()` は `===` であり、 `.toEqual()` は Jest が用意した、オブジェクトのプロパティを再帰的に `===` で調べていくメソッドである。

コンピューター言語にいろいろなレベルの等価性が存在することは基本事項なので、認識しておいてほしい。さらに学習したい場合は、コンピューター言語の学習に頻繁に使われる Scheme 言語を調べてみるのもいいだろう（→[等価性と比較 (Gauche ユーザリファレンス)](https://practical-scheme.net/gauche/man/gauche-refj/Deng-Jia-Xing-toBi-Jiao-.html)）。Common Lisp には 5 種類もの等価性判定関数が用意されている（[Equality in Lisp - Eli Bendersky's website](https://eli.thegreenplace.net/2004/08/08/equality-in-lisp)）。

##### 補足１

`===` が `true` を返すようにするには参照を代入すれば良い。そのときは `watch()` のコールバックは呼ばれない：

```typescript
const initial = { count: 0 }
const v = ref(initial)
watch(v, (cur) => console.log(cur.count))

v.value = initial  // 何も起こらない
```

##### 補足２

`reactive()` は、オブジェクトのプロパティを再帰的に走査して、全部のオブジェクトプロパティに `===` による比較を用いる監視機能を付ける。

##### 補足３

JavaScript 言語での `==` は、単純な `===` ではない便利な比較を用意しようとして提供されたものだが、 `== null` 以外の仕様が腐っているので、議論の対象外である。

##### 補足４

`ref()` の実際の実装では `===` ではなく `Object.is()` で等価性を判断しているが、話の都合上 `===` で実装したことにしてある。 `Object.is()` はほとんど `===` と同じ（→[Object.is() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is)）。

## TypeScript コンパイラーの型エラーの省略をしないようにする

TypeScript 型エラーが出る場合、コンソールに出る型が長い場合に `... 4 more ...` のように省略されて出力される。

この `... n more ...` の部分を見たい場合には tsconfig.json の `"compilerOptions": ` に `"noErrorTruncation": true,` を臨時に設定する。

常に設定すれば良いではないか、と思われるかもしれないが、エラー時の表示がとんでもないことになるのでやめておいたほうがいい。

## コンポーネントイベントと DOM イベント（カスタムイベントとネイティブイベント）

- **コンポーネントイベント**（カスタムイベント）は `$emit()` でトリガーされるイベントで、バブルアップしない。
- **DOM イベント**（ネイティブイベント）はブラウザーによってトリガー（または `element.dispatchEvent()` によってトリガー）されるイベントで、イベントごとにバブルアップするかどうか決められている。
- イベントハンドラーを記述したい場合には、組み込みの HTML タグに対しても、コンポーネントタグに対しても、**同じ表現** `v-on:eventname=` (または `@eventname=`) 属性を用いる。同じ表現でも、HTML タグに対して書いたときと、コンポーネントタグに書いたときとは**意味が異なる**ので注意が必要である。
- コンポーネントに対する `@eventname=` では DOM イベントのハンドラーを書くことはできない。
- どうしても書きたい場合は `<child-component @click.native="..." />` のように `.native` 修飾子を用いる。

（以下の例では `setup()` から `return { log: console.log }` してある）

```vue
<!-- parent.vue -->
<template>
  <div @click="log('click: outer')" @userclick="log('userclick: outer')">
    <child-component @click="log('click: parent')" @userclick="log('userclick: parent')" />
  </div>
</template>

<!-- child-component.vue -->
<template>
  <div @click="log('click: child root')" @userclick="log('userclick: child root')">
    <input type="button" value="click" @click="$emit('userclick')" />
  </div>
</template>
```

ボタンをクリックすると以下のように表示される：

```
userclick: parent  // (1)
click: child root  // (2)
click: outer       // (3)
```

1. emit による userclick イベントが parent.vue: `<child-component>` の `@userclick=` で処理された。バブルアップはしない。
2. バブルアップした DOM click イベントが child-component.vue: `<div>` の `@click=` で処理された。さらにバブルアップ。
3. バブルアップした DOM click イベントが parent.vue: `<div>` の `@click=` で処理された。さらにバブルアップ。

バブルアップした DOM click イベントが parent.vue: `<child-component>` の `@click=` で処理されないのはなぜだろうか？それは、`@click=` が HTML タグと Vue コンポーネントタグのどちらに書かれるかで意味が変わるからである。HTML タグに書くと DOM イベントに対するハンドラーを記述し、コンポーネントタグに書くと `emit` によるコンポーネントイベントのハンドラーを記述することになる。

コンポーネントタグで DOM イベントを処理したい場合、`.native` 修飾子を使うとコンポーネントタグに DOM イベントハンドラーを指定できる。

```vue
<!-- parent.vue -->
<template>
  <div @click="log('click: outer')" @userclick="log('userclick: outer')">
    <child-component @click.native="log('native click: parent')" @userclick="log('userclick: parent')" />
  </div>
</template>
```

クリックすると以下のように表示される。

```
userclick: parent
click: child root
native click: parent
click: outer
```

なお、`.native` 修飾子が記述できるのはコンポーネントタグに対してだけで、HTML タグに記述すると console に警告が表示される。また、コンポーネントタグに対して同一イベントの `.native` ありとなしの両方のハンドラーを記述することはできない。

次に child-component.vue の `<input>` のクリック処理でイベントのバブルアップを止めてみよう（`.stop` 修飾子を使う）。

```vue
<!-- child-component.vue -->
<template>
  <div @click="log('click: child root')" @userclick="log('userclick: child root')">
    <input type="button" value="click" @click.stop="$emit('userclick')" />
  </div>
</template>
```

クリックすると以下のように表示される。

```
userclick: parent
```

DOM click イベントのバブルアップが child-component.vue: `<input>` で止まるので、コンポーネントイベント userclick しか処理されない。そしてそれはバブルアップしないので、コンポーネントタグでのみ処理される。

では、`$emit()` するイベントを `'userclick'` から `'click'` に変えるとどうなるだろうか？→ 名前は DOM click イベントと同じでも、それはコンポーネントイベントなので、どのようにしてもバブルアップはしないし、 DOM イベントハンドラーで処理されることもない。それは parent.vue のコンポーネントタグ `<child-component>` においてのみ処理される。

```vue
<!-- parent.vue -->
<template>
  <div @click="log('click: outer')" @userclick="log('userclick: outer')">
    <child-component @click="log('click: parent')" @userclick="log('userclick: parent')" />
  </div>
</template>

<!-- child-component.vue -->
<template>
  <div @click="log('click: child root')" @userclick="log('userclick: child root')">
    <input type="button" value="click" @click="$emit('click')" />
  </div>
</template>

```

クリックすると以下のように表示される：

```
click: parent     // (1)
click: child root
click: outer
```

(1) の userclick が click に変わっただけである。parent.vue: `<child-component>` の `@userclick=` のかわりに `@click=` のハンドラーが実行された点だけが異なる。

(1) より下の `click: ` というのは `emit()` による click コンポーネントイベントではなく、`.stop` されなかった DOM click イベントがバブルアップしたものが処理された結果である。

その他 `.native` の利用や `.stop` による DOM click イベントのバブルアップ停止なども userclick のときと全く同じように動作する。ただ、名前が DOM イベントとコンポーネントイベントでかぶって紛らわしいだけである。

なお、コンポーネントイベントはバブルアップしないので、parent.vue: `<child-component @click.stop="...">` のように `.stop` を指定するのは全く意味がない。