自分用メモof自分用メモ
タイトルだけだと意味不明過ぎるが、状況としては
- とある関数に引数の一部の内容に応じて何らかの処理を付加した新しい関数を作りたい
といった状況を考える。
もう少し具体的に書くと、
const function1 = (
arg1: string,
arg2: {
foo: number,
bar: string,
},
): void => {
// 処理
}
const function2 = (
arg1: string,
arg2: {
foo: number,
},
arg3: number
): void => {
// 処理
}
// ...のように、引数の型として共通部分:
type TFunctionBase = (
arg1: string,
arg2: {
foo: number;
},
) => void;を持ちつつも、それ以外は異なる可能性のある関数たちがいるとして、 それらに対して共通の処理を行いたいような可能性を考える。 (共通したエラーハンドリングを設定するなど1)
TypeScriptに慣れていないと、
type TFunctionBase = (
arg1: string,
arg2: {
foo: number;
},
) => void;
const decorateFunction = <T extends TFunctionBase>(func: T): typeof func => {
return (...args: Parameters<T>): void => {
const [arg1, arg2] = args;
// arg1, arg2.foo に依存した処理を付加
func(...args); // オリジナルの処理
}
}
const sampleFunction = (
arg1: string,
arg2: {
foo: number;
bar: string[];
},
arg3: number,
): void => {
console.log('sampleFunction!', arg1, arg2, arg3);
};
/**
* decorateFunctionで付加した処理を行いつつ、
* オリジナルのsampleFunctionの処理を実行する
*/
const decoratedFunction = decorateFunction(sampleFunction);のような感じでやりたくなるが、
落ち着いて考えると上記のsampleFunctionはTFunctionBaseの部分型ではない。
関数型の部分型関係に関して引数は反変性を持つため、
返り値の型が同じであれば引数の型が厳しい方が関数としてはゆるい型、
つまり包含関係的に範囲が大きい。逆に、引数の型がゆるい方が関数としては厳しい型になる。
(引数がたくさんあるものについて一部を捨てて使うようなことは出来るが、逆に無い引数を使うことは出来ないことを考えれば感覚的に捉えられる)
ということで、「引数部分が部分型となる(必要最低限の引数を持っている)」という条件はもう少し複雑な書き方が必要になる。
具体的には、次のような形になる:
type TFunctionBase = (
arg1: string,
arg2: {
foo: number;
},
) => void;
/** 引数部分のタプル型 */
type TFunctionArgsBase = Parameters<TFunctionBase>
const decorateFunction = <T extends [...TFunctionArgsBase, ...unknown[]]>(func: (..._args: T) => void): typeof func => {
return (...args: T): void => {
const [arg1, arg2] = args;
// arg1, arg2.foo に依存した処理を付加
func(...args); // オリジナルの処理
}
}
const sampleFunction = (
arg1: string,
arg2: {
foo: number;
bar: string[];
},
arg3: number,
): void => {
console.log('sampleFunction!', arg1, arg2, arg3);
};
/**
* decorateFunctionで付加した処理を行いつつ、
* オリジナルのsampleFunctionの処理を実行する
*/
const decoratedFunction = decorateFunction(sampleFunction);のように修正すると今度はうまくいく。ポイントは
const decorateFunction = <T extends [...TFunctionArgsBase, ...unknown[]]>(func: (..._args: T) => void): typeof func => {
// ...
}の中でT extends [...TFunctionArgsBase, ...unknown[]]をしている部分。
これによって、TFunctionBaseの引数の部分型、すなわち引数のそれぞれがより詳しい属性を持っていたり、追加の引数を持っていても受け取ることが出来る。
Footnotes
-
個人的には、REST APIのhandlerの実装などで検討する機会があった。エンドポイント・メソッドが異なればfastifyのhandlerの引数: クエリ文字列、リクエストボディ、パスパラメータ、リクエストヘッダなどは一般には異なるが、全部バラバラにもならない(同一リソースに対する異なるメソッドなど)。「このパスパラメータを持つ場合はこういう検証を行う」、「DB操作を伴うものは最低限こういったエラーハンドリングを行う」などが具体的な検討対象。(なお、ここで記述しているやり方で対処すべきかどうかは別問題) ↩