オプション引数なんて可能な限り減らすべきだよなと思いつつ、うまい実装方法はないものかと考えるのをやめられません。楽しいですし。
TypeScript では引数にデフォルト値を指定でき、かつ引数オブジェクトの destructuring も行えそこにもデフォルト値を指定できるので、きっとこれを利用したオプション値の設定手法も何かしらあるのだろうと調べてみたところ 2015/11 (結構前だ...)の記事を見つけたので、真似してやってみました。
Typing Destructured Object Parameters in TypeScript
二つのフィールド o1
と o2
を持つオプションオブジェクトを func
の第二引数に渡しています。
function func(v: any, {
o1 = 'default-value',
o2 = 1000,
}: { o1?: string, o2?: number } = {}) {
console.log(o1, o2);
}
個々のプロパティが省略されている場合はそれぞれのデフォルト値を適用し、オプションオブジェクト自体が省略されている場合は空のオブジェクト {}
が適用され、次いで個々のプロパティのデフォルト値が適用される、みたいな処理順なのでしょうか。
これで期待通りに動作するのでそういうことなのでしょうね。
JavaScript では
吐き出された JavaScript のコードを見てみると
function func(str, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.o1, o1 = _c === void 0 ? 'default-value' : _c, _d = _b.o2, o2 = _d === void 0 ? 1000 : _d;
console.log(o1, o2);
}
のようになっています。デフォルト値の部分を頑張って折り返すと
// オプションオブジェクト自体のデフォルト値処理
var _b = (_a === void 0) ? {} : _a,
// o1 のデフォルト値処理
_c = _b.o1,
o1 = _c === void 0 ? 'default-value' : _c,
// o2 のデフォルト値処理
_d = _b.o2,
o2 = _d === void 0 ? 1000 : _d;
ですね。ほどいてみると意外に整然としていてびっくりしました。式中に出てくる void 0
は__常__に__本当__の undefined
を返す JavaScript のイディオムみたいです(参考)。
Partial を使ってみる
ちなみにですが、最近使えるようになった TypeScritp の Partial
を利用すると
function func(v: any, {
o1 = 'default-value',
o2 = 1000,
}: Partial<{ o1: string, o2: number }> = {}) {
console.log(o1, o2);
}
こう書けたりもしますね。メンバ全てがオプショナルな場合は ?
をつけ忘れるのを気にしなくても済むので私はこちらの方が好きです。
インタフェース定義を分ける
さすがにごちゃごちゃしているのでインタフェース定義だけ別に書くとこんな感じになります。
interface Options {
o1: string,
o2: number,
}
function func(v: any, {
o1 = 'default-value',
o2 = 1000,
}: Partial<Options> = {}) {
console.log(o1, o2);
}
まずまずの読みやすさでしょうか。
ネストした状態のものも色々試したり探したりしていたのですが見つからなかったので今回はここまでです。
ネストする
...で、後日ですが見つけました。ネストすると邪魔なので Partial
は外してしまいました。
type Options = {
o1?: string,
o2?: number,
o3?: { // ネストしたオプション定義
o3_a?: string; //
o3_b?: number; //
} //
}
function func(v: any, {
o1 = 'default-value',
o2 = 1000,
o3: { // ネストしたオプション
o3_a = 'nested-value', //
o3_b = 2000, //
} = {} // o3 用の空オブジェクト
}: Options = {}) { // オプション全体用の空オブジェクト
console.log(o1, o2);
}
=
と :
の使い分けがかなりややこしかったです... オプションオブジェクトには__「内部のデフォルト値定義を行ってから空オブジェクト」__が基本構造のようですね。こちらも出力された JavaScript を覗いてみると
function func(v, _a) {
// オプションオブジェクト全体が定義されていない際ののデフォルト値処理
var _b = _a === void 0 ? {} : _a,
_c = _b.o1, o1 = _c === void 0 ? 'default-value' : _c,
_d = _b.o2, o2 = _d === void 0 ? 1000 : _d,
// ネストしたオプションオブジェクト `o3` 自体が定義されていない際のデフォルト値処理
_e = _b.o3, _f = _e === void 0 ? {} : _e,
// `o3` のプロパティのデフォルト値処理
_g = _f.o3_a,o3_a = _g === void 0 ? 'nested-value' : _g,
_h = _f.o3_b, o3_b = _h === void 0 ? 2000 : _h;
console.log(o1, o2);
}
長くなるので折り返し方をちょっと変えました。特に問題なくネストした部分も処理されていますね。
そしてこれも後日気づいたのですが --target ES2015
を指定するとそもそもデフォルト値処理の部分は特に変更されずに吐き出されるんですね... でも処理の順序が理解できたので良かったです。