typescriptの型拡張型でライブラリのメソッド入力に制限を行う

今回はよく使う便利な型拡張のパターンを紹介します。

# 型拡張のやり方 

今回題材として、react-navigation（今回諸事情で1系なのはご了承ください。）のnavigateメソッドをあつかいます。

このメソッドの第一引数はstring型なのですが、実際は登録したRoute以外の文字列を引数に入力すると、何も起こらず望んだ挙動になりません。

そこで、入力できる引数を自分たちが定義した文字列に限るようにするというのが今回のテーマです。こういった入力制限を型拡張でできることを覚えておくととても便利です。

以下はambientファイルと呼ばれるtypescriptの型定義ファイルです。これはtypescriptのincludeパスに入っているとコンパイル前に自動でこの型定義を使用してコンパイルしてくれるc言語でいうヘッダファイルのようなものです。module宣言はその後に続く文字列で指定したパスをimportしているソースファイルに対して、型定義の解析をしてからimportを行なってくれるため、既存のライブラリに存在する型定義に拡張を施すことができます。

関数のオーバーロードでは、さらに、入力引数に対する制約条件をいれることができるため、これによって、

```react-navigation.d.ts
export * from 'react-navigation';
import { Routes } from '~/components/route';

declare module 'react-navigation' {
  export type RouteName = Routes;

  export namespace NavigationActions {
    function navigate<T>(options: T): NavigationNavigateAction;
  }

  export type TSRouteConfigMap = Partial<{ [routeName in RouteName]: NavigationRouteConfig }>;

  export function TabNavigator(routeConfigMap: TSRouteConfigMap, drawConfig?: TabNavigatorConfig): NavigationContainer;

  export function DrawerNavigator(
    routeConfigMap: TSRouteConfigMap,
    drawerConfig?: DrawerNavigatorConfig
  ): NavigationContainer;

  export function SwitchNavigator(
    routeConfigMap: TSRouteConfigMap,
    switchConfig?: SwitchNavigatorConfig
  ): NavigationContainer;

  export function StackNavigator(
    routeConfigMap: TSRouteConfigMap,
    stackConfig?: StackNavigatorConfig
  ): NavigationContainer;

  export interface NavigationScreenProp<S, P = NavigationParams> {
    navigate<T>(options: { routeName: T; params?: NavigationParams; action?: NavigationAction; key?: string }): boolean;
    navigate<T>(routeNameOrOptions: T, params?: NavigationParams, action?: NavigationAction): boolean;
  }
}
```
また　components/routes.tsの中身は例えば、以下のように定義されています。

```routes.ts
// .../部分抜粋
export type Routes =
  'Entrance'
  | 'Entrance/SignUp'
  | 'Entrance/Login'
  | 'Entrance/SignUp/MailSignUp'
  | 'Entrance/SignUp/Register'
  | 'Entrance/Register'
```

## 呼び出し先で何が起きるか

以上がうちのチームで導入指定しているreact-navigationの型定義になり複雑そうに見えますが、やってることはRouteの登録時に行うrouteConfigMapの制約条件として、RouteNameという新たな型を導入しそこで定義されている文字列以外にkeyにすることができない制限と、navigateメソッドを使用する可能性がある型のnavigateメソッドにGeneric型を適用しているだけです。なぜnavigateメソッドの引数をRouteNameのオーバーロードにしないかというと、オーバーロードにすると、navigateはすでにstringを許容するメソッドであるため、補完をすることができても入力制限まではできませんが、navigate<RouteName>という型パラメータ付きで、実行することでなく文字列補完だけではなく、入力制限まで行うことができるからです。

以下はGeneric補完で、ジェネリックメソッドに型パラメータRouteを指定したことによりstring型のオーバーロードの影響を受けず警告が出ている図
<img width="559" alt="スクリーンショット 2018-11-25 18.51.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/99145/ea988efd-dccc-0fb6-73ec-f42de3102f24.png">
以下は、オーバーロード引数stringが有効になってしまっているため警告が出ていない図です。
<img width="493" alt="スクリーンショット 2018-11-25 18.53.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/99145/d13f3b33-0143-af40-2b83-c4e20253a8dc.png">

createNavigatorの引数に対してのRoutesNameの制限に関しては、以下のように、独自定義型なので、一度変数にいれて、検証することで、解決していますがこちらも、Genericにして解決しても構いません。

<img width="235" alt="スクリーンショット 2018-11-25 19.03.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/99145/c951f766-bf16-ed70-e75c-285d9e063fee.png">


以上のようにすでに、string型で入力可能な状態になっているライブラリ内のメソッドであってもGeneric型をうまく使いこなすことによって、このテクニックは様々な場面で大活躍するので、覚えておいて損はないでしょう。