関数fは、オブジェクトを引数にとります。オブジェクトには、a、bというプロパティがあります。コードにするとこんな感じですね。
interface P {
a: string;
b: number;
}
function f(p: P) {
}
この時に省略可能にするには、 ?
を使えばOKですよね。これはTypeScriptを使っているだいたいの人は知っていることかと思います。
interface P {
a: string;
b?: number; // bは省略可能
}
逆に、余計な要素が入っていても問題ないようにするにはどうすればいいでしょうか?2つ方法があります。
インタフェース側に手を加える方法
オブジェクトを汎用のMapがわりに使う記法を使うというのがその1つです。これで、Pには何を入れることもできます。そのため、エラーとはならなくなります。
interface P {
a: string;
b?: number;
[extraProps: string]: any; // これでどんな属性も受け取れるようになる。
}
function f(p: P) {
}
欠点が1つあります。オプションの属性として型定義するときに、余計な属性が入っていると、ユーザーからすると「これはなんだろう?」と疑問を覚えるポイントになるでしょう。これはどんな意味があるのか、どういう影響を与えるのか。コメントが必要になるポイントな気がします。
関数側に手を加える方法
もう一つは関数宣言の方でインタフェースの継承とジェネリクスでなんとかする方法もあります。
// こっちは変更なし
interface P {
a: string;
b?: number;
}
function f<P2 extends P>(p: P2) {
}
これ、何をしているかというと、未知の型P2を受け取る、という宣言です。P2はPを継承した型です。Pは外で宣言されているもので、属性はすでにわかっています。P2はPを継承しているので、Pが持っている属性はすべて得られる。なおかつ、P2独自の何か特別な属性を持っているかもしれない、という感じの宣言になります。ようするに、Pを内包するものなら受け取れる、ということで、今回のケースではうまく当てはまります。
欠点が2つあります。
dtslintという純正ツールがno-unnecessary-genericsというエラーを出してきます。なので、このツールが必須のDefinitelyTypedにPRを送る時には使えません。
もう一つは、デフォルト引数の分割代入と一緒に使えない点ですね。
// error TS2322: Type '{ a: string; }' is not assignable to type 'P2'.
function f<P2 extends P>(p: P2 = { a: "" }) {
}
p?: P2
と書けばいけるので、デフォルト値の処理を手書きすればなんとかいけますが・・・
どちらがいいの?
どちらも、省略可能な属性のスペルミスをすると気づけない、という欠点は残ります。で、それ以外の要素を見比べた時にどちらがいいのか?というと、基本的には後者の方が筋がいいように思います。
というのも、今回は「余計な要素が入った時も関数で受け取れるように柔軟にしたい」というのが関心ごとです。前者では、引数の情報そのものが増えたという意味になりかねません。ただ、属性は増えてません。なので、可能なら後者一択ですね。ただ、可能じゃないこともあるので、ダメなら前者にフォールバック・・・ですかね。