高度な型操作 - Typescript & Angular 勉強会 #9



## ルックアップ型
以下のような型が定義されていたとします。

```ts
type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: {
                firstName: string
                lastName: string
            }[]
        }
    }
}
```

この型の `friends` 部分だけを利用したい場合、その型はどのように書けるでしょうか。
普通に考えると、その部分の型を以下のように抜き出して、

```ts
type Frined = {firstName: string, lastName: string}
```

その型を `APIResponse` に組み込むのが良いと考えるでしょう。

```ts
type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: Friend[]
        }
    }
}
```

しかし、上記のような手段が取れないようなこともあります。例えば、`APIResponse`型をSwaggerCodegenなどを利用して自動生成しているような場合は、自動生成コードに手を入れることはできないので、上記のような方法は使えません。

またもっとシンプルに、`APIResponse`型がnpmで取ってきたライブラリ内の型で、自分たちでは手を出せない、ということもあるでしょう。

そのような場合に有効なのが**ルックアップ型**です。

この場合、以下のようにかけます。

```ts
type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: {
                firstName: string
                lastName: string
            }[]
        }
    }
}

// ルックアップ型で型の部分集合へアクセス
type Friend = APIResponse['user']['friendList']['friends'][0]
```

このように、配列アクセスのような形で、型のメンバの部分型へとアクセスできるのがルックアップ型の特徴です。

---

## レコード型
例えばエラーコードに対応したメッセージを定義したい、という場合、以下のように書くことが多いでしょう。

```ts
type HttpErrorCode = "400" | "404" | "500"
const messages = {
    "400" : "引数エラー",
    "404" : "見つかりません",
    "500" : "サーバーエラー"
}
```

もちろん上記は型安全ではありません。 `messages` には `HttpErrorCode` で定義していないエラーコードへのメッセージもかけてしまいます。

これを型安全にするのがレコード型です。

```ts
type HttpErrorCode = "400" | "404" | "500"
const messages: Record<HttpErrorCode, string> = {
    "400" : "引数エラー",
    "404" : "見つかりません",
    "500" : "サーバーエラー"
}
```

こうすることで、型安全性はもちろん、完全性まで手に入ります。それは以下のようなコードで発生するコンパイルエラーによりわかります。

```ts
type HttpErrorCode = "400" | "403" | "404" | "500"

// Property '403' is missing in type '{ "400": string; "404": string; "500": string; }' but required in type 'Record<HttpErrorCode, string>'. 
const messages: Record<HttpErrorCode, string> = { 
    "400" : "引数エラー",
    "404" : "見つかりません",
    "500" : "サーバーエラー"
}
```

上記のように、**全ての取りうる値を満たさない限り、コンパイルエラー**としてくれます。

また、ほぼ同等の機能を有する、、、というかそれのさらに上位の機能を持つ型としてマップ型があります。

---

## マップ型
レコード型で実現したコードと同等なコードをマップ型で書くと以下のようになります。

```ts
// Property '403' is missing in type '{ "400": string; "404": string; "500": string; }' but required in type 'Record<HttpErrorCode, string>'. 
const messages: {[K in HttpErrorCode]: string} = {
    "400" : "引数エラー",
    "404" : "見つかりません",
    "500" : "サーバーエラー"
}
```

こちらのコードでも、レコード型と同様にプロパティが足りない旨をコンパイルエラーとしてくれます。

ではマップ型独自の機能とはなんでしょうか。

マップ型とレコード型を見比べて明確に違うのは、定義部分が通常のクラス定義に近い形になっている、ということです。
見やすく書くと、以下のようになります。

```ts
type ErrorMap = {
    [K in HttpErrorCode]: string //オブジェクト定義っぽい
}
```

実際にここはオブジェクト定義となっており、例えばここに省略可能メンバを示す `?` を入れるなど、独自の定義を挿入することができます。

```ts
type OptionalErrorMap = {
    [K in HttpErrorCode]?: string
}
```

実はこの機能を応用すると、既存の型を簡単に拡張できるようになります。
例えば、全てのフィールドを省略可能にする `Partial`、 全てのフィールドを必須にする `Required`、特定のKeyだけ抜き出す `Pick` などです。

これらをうまく使うと、無駄な多重定義をなくしてコードの重複を少なくすることができます。

例えばWebAPIのBodyに利用するクラスを考えます。
Post、Get、Patchなどの各APIに対応する方を考えると、非常に冗長な定義が発生します。

```ts
type UserPost = {
    name: string,
    birthday: Date
}

//Getには作成日が付加される
type UserGet = {
    name: string,
    birthday: Date,
    createdAt: Date
}

//Patchは任意のプロパティを省略可能
type UserPatch = {
    name?: string,
    birthday?: Date
}
```

しかし、これを合併型と、特殊なマップ型を利用すると、以下のようにできます。

```ts
type UserPost = {
    name: string,
    birthday: Date
}

//Getには作成日が付加される
type UserGet = UserPost & {
    createdAt: Date
}

//Patchは任意のプロパティを省略可能
type UserPatch = Partial<UserPost>
```

---

## 名前的型のシミュレート
Typescriptの型システムは構造的部分型である、という話は何度かしてきたと思います。改めて一言で解説すると、**Typescriptは同じ名前と型のプロパティを持っている型同士を同一の型として扱います。**

例えば以下のようにID型を定義したとしても、

```ts
type CompanyID = string
type OrderID = string
type UserID = string
```

このように異なる型同士でも代入ができてしまいます。

```ts
let orderID: OrderID = "123456"
let companyID: CompanyID = orderID //OK
```

これをコンパイルエラーとするためには、型定義に**型のブランド化**というテクニックを導入します。

```ts
type CompanyID = string & {readonly brand: unique symbol}
type OrderID = string & {readonly brand: unique symbol}
type UserID = string & {readonly brand: unique symbol}
```

一見、同じような型が定義されただけのように見えるかもしれませんが、Typescriptにおいて `unique symbol`型は特異的な性質を持ちます。`unique symbol`型は、アプリケーション内において絶対に衝突しない型となっています。つまり、この型だけはTypescriptの「構造的部分型」の範疇外で、このプロパティを含む型は同一のプロパティを持っていたとしても別の型として扱われます。

しかし、今度は`CompanyID`にString型は代入できなくなりました。なので、文字列型をそれぞれの型にキャストする関数も新設します。

```ts
type CompanyID = string & {readonly brand: unique symbol}
const CompanyID = (id: string) => { return id as CompanyID }
type OrderID = string & {readonly brand: unique symbol}
const OrderID = (id: string) => { return id as OrderID }
type UserID = string & {readonly brand: unique symbol}
const UserID = (id: string) => { return id as UserID }
```

こうすることで、異なるIDへの割当がエラーになります。

```ts
let orderID: OrderID = OrderID("123456")
let companyID: CompanyID = orderID // Type 'OrderID' is not assignable to type 'CompanyID'.
```

このように素の型をできるだけ使わず、独自の型を定義し、異なる方への代入をコンパイラレベルで検知することで、安全性を大きく増すことが可能になります。

---

## 更に学習したい人へのリソース
<a href="https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0TypeScript-%E2%80%95%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8BJavaScript%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%96%8B%E7%99%BA-Boris-Cherny/dp/4873119049/ref=as_li_ss_il?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Typescript&qid=1603237254&sr=8-1&linkCode=li2&tag=posaunehm-22&linkId=9d4bf5f38ff67ae1167085b247750777&language=ja_JP" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=4873119049&Format=_SL160_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=posaunehm-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=posaunehm-22&language=ja_JP&l=li2&o=9&a=4873119049" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

勉強会資料のもととなった書籍です。実は内容としてはようやく半分と言ったところで、更に深い型の話もあります。

- [Typescript DeepDive](https://typescript-jp.gitbook.io/deep-dive/)
    - 基礎からマニアックな事柄までカバーされたTypescriptのオンライン本です。こちらもより深く学ぶのにおすすめです。

- [TypeScript 型の課題集](https://github.com/type-challenges/type-challenges/blob/master/README.ja.md)
    - Typescriptの型システムについて学ぶ課題集。はっきり言ってかなりマニアックで、実用性があるかと言われるとライブラリ作者以外にはないかも、、、。ただパズルとしては面白く、型とコンパイラでここまでできるのか！というのの例には良いのかなと。