公称型をTypeScriptで《より型安全》に実現する方法

先日、[公称型をTypeScriptで実現するための基礎](https://qiita.com/suin/items/ae9ed911ebab48c98835)という投稿をしました。そこでは、「部分的構造型」というタイプに分類される型システムを持つTypeScriptで、JavaやPHPなどが採用している「公称型」はどうやったら実現できるのかについてお話しました。

ざっくりおさらいすると、3つのテクニックを組み合わせるというものでした。

* テクニック1: 構造を微妙にずらして型同士の互換性を崩そう
* テクニック2: 構造ずらしには`unique symbol`を使おう
* テクニック3: 変数の生成するための関数を作って、変更に強くしよう

これらのテクニックを実践したサンプルコードは次のようになります。

```typescript:book.ts
export type Book = {
  name: string
  [bookNominality]: never // テクニック1
}
declare const bookNominality: unique symbol // テクニック2
// テクニック3
export function newBook(name: string): Book {
  return { name } as Book
}
```
```typescript:person.ts
export type Person = {
  name: string
  [personNominality]: never // テクニック1
}
declare const personNominality: unique symbol
export function newPerson(name: string): Person {
  return { name } as Person
}
```
```typescript:利用者側コード.ts
import { Book, newbook } from './book.ts'
import { Person, newPerson } from './person.ts'
let book: Book = newBook('オライリー')
let person: Person = newPerson('Bob') 
book = person // 期待通りコンパイルエラーになる
```

このテクニックは、概ね実用レベルですが、弱点についてもお話しました。それは、`as`の部分だけが型安全性が低下するというところです。例えば、`Book`型に`price`プロパティが追加された場合、`newBook`関数も直さないといけません。

このとき、直す必要がある箇所はコンパイラが教えてくれると、修正忘れがなくなりいいのですが、`as`が書かれているせいでコンパイラは何も教えてくれなくなります。

```typescript
export type Book = {
  name: string
  price: number // 追加
  [bookNominality]: never //
}
export function newBook(name: string): Book {
  return { name } as Book // priceが足りなくてもコンパイルエラーにはならない……
}
```

`newBook`関数は`Book`型の定義に近い場所に書いてあるため、修正漏れのリスクはだいぶ低いとは思いますが、それでもリスクがゼロになったわけではありません。

## 公称型をTypeScriptを型安全に実現する方法

本稿では、`{ name } as Book`をより型安全にする、つまり、コンパイル時に修正漏れに気づきやすくする方法を考えていきたいと思います。

それでは、問題箇所の

```typescript
export function newBook(name: string): Book {
  return { name } as Book
}
```

をリファクタリングしていきましょう。

まず、`as Book`の目的を考えます。これは、`bookNominality`の値を代入せずに、`Book`型のオブジェクトを作りたいがために書いています。

もっというと、`bookNominality`以外のすべてのプロパティがそろったオブジェクトに限り、`Book`型として扱いたいというのがコードの意図になります。

この意図をより正確に表すために、`bookNominality`以外すべてのプロパティを持ったオブジェクトの型を定義します。特定のプロパティを除いた型を定義するには、`Omit`を使うと便利です。

```typescript
type BookLike = Omit<Book, typeof bookNominality>
```

`BookLike`型は`bookNominality`を除いたオブジェクトなので、`name`と`price`をちゃんと持っているオブジェクトであればコンパイルが通ります。

```typescript
export interface Book {
  name: string
  price: number
  [bookNominality]: never //
}
type BookLike = Omit<Book, typeof bookNominality>
// ↓コンパイル通ります
const book: BookLike = {
  name: 'オライリー',
  price: 1000
}
```

次に、`BookLike`を受け取り、`Book`を返す関数を作ります。

```typescript
function newBookSafe(bookLike: BookLike): Book {
  return bookLike as Book
}
```

この関数でも`as`を用いているので、ここは型安全性が弱まっていますが、`BookLike`型がすでに`bookNominality`以外のプロパティを持った`Book`型であることを保証しているので、`newBook`関数の`{ name } as Book`よりは安心して扱うことができます。また、`BookLike`は自動的に、`Book`の構造変化に追従していくので、今後`newBookSafe`の実装を修正する必要が出てくるこのはありません。

最後に、`newBookSafe`関数を`newBook`関数から呼び出すように修正すれば、リファクタリングは完了です。

```typescript
export function newBook(name: string): Book {
  return newBookSafe({ name }) // ここでpriceが無いことがコンパイルエラーで指摘されるようになります。
}
```

リファクタリング後の完全なコードは次のようになります。

```typescript:book.ts
export interface Book {
  name: string
  price: number
  [bookNominality]: never //
}
declare const bookNominality: unique symbol

type BookLike = Omit<Book, typeof bookNominality>
function newBookSafe(bookLike: BookLike): Book {
  return bookLike as Book
}

export function newBook(name: string): Book {
  return newBookSafe({ name }) // 「priceが足りないよ」とコンパイラが教えてくれます。
}
```

このコードは実際に[TypeScript Playground](http://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgIQggGs4BvAKDjiUwFtgAuOAZximQHMq4wPdmSAK50ARmh4BtUUWIA5CHWSYANghgBPALqDgANwkBfCgBNg2FZih5sEJGzgySCpbTWbmQpAgCOQvCwaYhAqFBSaYHiEJAAyCMR4ALxwAPJKMAA80cQANHARwBDojrIuyu4aAHwU6F7YMAh2NMAA7tkAypjowAAUTsRxCczZg8AAlMOy5DzWMEJQSCWx8XiYLASyFMYUoJCwcLVI9Y2LSK3ZPbQMzGwcSJwTGyTT1LPzp+eynd09ZDT0eEMYzgAHoQXBADAMfAQuEAMgyAZtjAFEMgCsGQAiDIAIhkAsAyACwZAM0MgGeGQCLDIAShkAlwywwCapoAJBkAZgyAeQZADEMgD8GQCaDIAgBi2QA)で動かしてみることができます。

## まとめ

本稿では、[公称型をTypeScriptで実現するための基礎](https://qiita.com/suin/items/ae9ed911ebab48c98835)で取り上げた手法の弱点だった型安全性の弱まりを軽減する方法を説明してきました。

そのアプローチとしては、

* 公称型のために定義されたプロパティを除いた、型を定義する
* その型を`as`で型変換する

という方法でした。

このアプローチはたしかに型の安全性は高まったと言えますが、デメリットとしては、公称型を実現するための型定義や関数が増えてしまうという点です。先の`Book`の例でいうと、

```typescript
type BookLike = Omit<Book, typeof bookNominality>
function newBookSafe(bookLike: BookLike): Book {
  return bookLike as Book
}
```

の部分です。今回は`Book`型しか扱わなかったので、4行追加するだけのリファクタリングで収まりましたが、公称型にしたい型の数だけこのリファクタリングをやるとなると、同じようなコードをいたるところで書かないといけなくなり、かなり面倒になることが予想されます。

次回は、更にリファクタリングを進めて、型安全性を今回のアプローチと同等に保ちつつ、コードの再利用性を高める方法を紹介していこうと思います。

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)