空雲 Blog

Eye catchTypeScriptのGenericsで部分的型推論を行う

publication: 2023/04/15
update:2024/02/20

TypeScript では部分的型推論が出来ない

TypeScript では部分的型推論が出来ないというのは、Generics の使い方に制限を与えてしまう問題です。例えば、上記のコードでは func 関数に対して一つ目の引数だけ number 型を指定したい場合、残りの引数の型も明示的に書かなければなりません。しかし、これは冗長であり、型安全性も損なわれる可能性があります。なぜなら、引数の型が変わったときに、Generics の型も一致させる必要があるからです。

部分的型推論が出来ればこのような問題を回避できまが、TypeScript では以下のような状態です。

1const func = <A, B, C>(a: A, b: B, c: C): [A, B, C] => {
2 return [a, b, c];
3};
4
5func(1, "2", 3); // OK
6func<number>(1, "2", 3); //NG
7func<number, string, number>(1, "2", 3); //OK

Generics を使うと引数の推論によって型を取り出し加工して再利用出来るので、複雑な変換を扱うデータを記述することも容易になります。ただし問題があって、対象の関数を利用するとき Generics に対して一つでも型を与えてしまうと、全て指定しなければなりません。型を一つだけ指定して、残りは推論してもらいたいという機能が無いのです。

外部から型を注入したい場合に困る

例えば以下のような JSON ファイルからデータを読み取って、対象のデータを取り出すというプログラムがあったとします。

1import fs from "fs";
2
3// jsonファイルからデータを読み込む
4const getValue = <T extends object, K extends keyof T>(
5 fileName: string,
6 keyName: K
7): T[K] => {
8 const text = fs.readFileSync(fileName);
9 const data = JSON.parse(text.toString());
10 return data[keyName];
11};
12
13// ファイル内の型構造は分かっている
14type DataType = { a: number; b: string };
15
16// 推論を一つ設定すると、残りも手動で設定しなければならない
17const value1 = getValue<DataType, "a">("file.json", "a"); // value1:number
18const value2 = getValue<DataType, "b">("file.json", "b"); // value2:string
19// `a`を自動推論したいが拾ってこられない
20const value3 = getValue<DataType>("file.json", "a"); // タイプが一つだけしか指定されていないのでError

getValueは汎用的に使えるようにしたいけれど、特定のファイルの内部構造は分かっているので型を指定したい。そして取得したデータの型は自動で付けたい。こういう場合にabの部分は自動で推論してもらいたいのですがこれは出来ません。手動で指定する必要があります。

この問題を解決するために、TypeScript では Partial Type Argument Inference という機能が提案されています。これは、Generics に対して型を部分的に指定することができるようにするものです。しかし未だ使えない以上は、ある機能で対応するしかありません。

部分的型推論の作り方

以下のように型を指定する部分と推論部分を分けた type を作ります。interface でも同じ事は可能です。

1// Tを設定しKは自動推論のタイプを作る
2type GetValue<T extends object> = {
3 <K extends keyof T>(fileName: string, keyName: K): T[K];
4};
5
6// キャストにより部分推論が出来る
7const value4 = (<GetValue<DataType>>getValue)("file.json", "a"); // value4:number
8const value5 = (<GetValue<DataType>>getValue)("file.json", "b"); // value5:string
9
10// 型設定済み関数を作る
11const getValue2: GetValue<DataType> = getValue;
12const value6 = getValue2("file.json", "a"); // value6:number
13const value7 = getValue2("file.json", "b"); // value7:string

先ほどの getValue をキャストすることによって、部分的型推論が機能するようになりました。書き方が冗長になってしまうので、いったん部分的型推論済みのgetValue2を作るという書き方も可能です。

外部から型情報を持ってくる必要のある汎用ライブラリなどを作る場合は、この方法を使うとかなり便利になります。

まとめ

部分的型推論の標準対応が欲しい。