はじめに
TypeScriptの関数型について、いまいちすっきりしてなかったので整理してみました。
まず、準備としてとして
type IHoge = {
a: number;
b: string;
}
とします。
関数定義の基本パターン
次に、関数定義の基本パターンとして
function func1(a: IHoge): IHoge {
return { a: 3, b: 'a'};
}
とします。このときfunc1の型は「function func1(a: IHoge): IHoge
」です。
以降、このバリエーションを見ていきます。意味的にはすべて同じです。
アロー式での関数定義
//-------------------------これ以上できないぐらいの型のフル指定(アロー式リテラル版)。------------------
const func2: (a: IHoge) => IHoge
= (a: IHoge): IHoge => ({ a: 3, b: 'a'}) ;
アロー式で定義します。このときfunc2の型は「const func2: (a: IHoge) => IHoge
」です。
func1のfunction型とは型としては異なることがわかります。しかし両者の型は実質的には同じで、以下のように代入が可能です。
const func2_a: (a: IHoge) => IHoge = func1;
「これ以上できないぐらいの型のフル指定」では、func2というシンボルの型と、関数本体の定義の方で、関数と引数と返り値の型を二重に指定していることがわかると思います。これは無駄なので、省略を試みます。
まず、シンボルの型を省略してみます。省略された場所は「/**/
」で示します。
//-------------------------以下の2つは型推論によりほぼ同じ(アロー式リテラル版)------------------
const func3 /**/
= (a: IHoge): IHoge => ({ a: 3, b: 'a'}) ;
const func4 /**/
= (a: IHoge) /**/ => ({ a: 3, b: 'a'}) ;
のようになります。戻り値の型を省略したfunc4の場合、戻り値の型は関数本体から推論されていることになります。func4の戻り値の型は「{
」で、IHogeとはノミナルな意味では一致しませんが、TypeScriptでは特に問題はありません。しかし一般には、この推論があたっているとは限らないので、func3のように明示したいところです。型チェックの意義が薄れるからです。
a: number;
b: string;
}
次、引数の型を省略を試みます。
//-------------------------引数は型推論しないので型指定すべき(アロー式リテラル版)------------------
const func5 /**/
= (a: any/**/) : IHoge => ({ a: 3, b: 'a'}) ;
引数の型を省略すると、TypeScriptは呼び出し側の引数から型推論をすることはないので、「Parameter 'a' implicitly has an 'any' type.」というエラーになると思います(オプションによる)。ここではanyを補っていますが、こういうことをすると、引数の型チェックがなされなくなるので、やめるべきです。func1よりもコンパイル時に型エラーチェックできる能力が減損しています。
次に、シンボルの型の方ではなく、関数定義本体の方の型を省略してみます。
//-------------------------以下の三つは同じ(アロー式リテラル版)------------------
const func6: (a: IHoge) => IHoge
= (a /**/) /**/ => ({ a: 3, b: 'a'}) ;
const func7: (a: IHoge) => IHoge
= (a /**/): IHoge => ({ a: 3, b: 'a'}) ;
const func8: (a: IHoge) => IHoge
= (a : IHoge) /**/ => ({ a: 3, b: 'a'}) ;
これでわかるように、関数定義本体の方の型は省略しても不都合がありません。
func7, func8は二重指定になっています。
個人的にはfunc6のパターンがお勧めです。
なお、func2〜3, 6〜8の型は「const func*: (a: IHoge) => IHoge
」です。
func4の型は「const func11: (a: IHoge) => {
」です。
a: number;
b: string;
}
func5の型は「const func5: (a: any) => IHoge
」です。
functionリテラル形式での関数定義
さて、今まではアロー式で型を与えてきましたが、まったく同じことがfunction形式の関数でも言えます。
//-------------------------これ以上できないぐらいの型のフル指定(functionリテラル版)。------------------
const func9: (a: IHoge) => IHoge
= function(a: IHoge): IHoge { return ({ a: 3, b: 'a'}) }
//-------------------------以下の2つは型推論によりほぼ同じ(functionリテラル版)------------------
const func10 /**/
= function(a: IHoge): IHoge { return ({ a: 3, b: 'a'}) };
const func11 /**/
= function(a: IHoge) { return ({ a: 3, b: 'a'}) }
//-------------------------引数は型推論しないので型指定すべき(functionリテラル版)------------------
const func12 /**/
= function(a: any/**/) : IHoge { return ({ a: 3, b: 'a'}) }
//-------------------------以下の三つは同じ(functionリテラル版)------------------
const func13: (a: IHoge) => IHoge
= function(a /**/) /**/ { return ({ a: 3, b: 'a'}) }
const func14: (a: IHoge) => IHoge
= function(a /**/): IHoge { return ({ a: 3, b: 'a'}) }
const func15: (a: IHoge) => IHoge
= function(a : IHoge) { return ({ a: 3, b: 'a'}) }
個人的おすすめはこのfunc13のパターンです。
このとき、func9〜10, 13〜15の型は「const func*: (a: IHoge) => IHoge
」です。(functionではない)。
func11の型は「const func11: (a: IHoge) => {
」です。
a: number;
b: string;
}
func12の型は「const func12: (a: any) => IHoge
」です。
以下はテストコード。
const d = {a: 3, b: 'abc'};
const x1: number = func1(3); const y1: number = func1(d); const z1: IHoge = func1(d);
const x2: number = func2(3); const y2: number = func2(d); const z2: IHoge = func2(d);
const x3: number = func3(3); const y3: number = func3(d); const z3: IHoge = func3(d);
const x4: number = func4(3); const y4: number = func4(d); const z4: IHoge = func4(d);
const x5: number = func5(3); const y5: number = func5(d); const z5: IHoge = func5(d);
const x6: number = func6(3); const y6: number = func6(d); const z6: IHoge = func6(d);
const x7: number = func7(3); const y7: number = func7(d); const z7: IHoge = func7(d);
const x8: number = func8(3); const y8: number = func8(d); const z8: IHoge = func8(d);
const x9: number = func9(3); const y9: number = func9(d); const z9: IHoge = func9(d);
const x10: number = func10(3); const y10: number = func10(d); const z10: IHoge = func10(d);
const x11: number = func11(3); const y11: number = func11(d); const z11: IHoge = func11(d);
const x12: number = func12(3); const y12: number = func12(d); const z12: IHoge = func12(d);
const x13: number = func13(3); const y13: number = func13(d); const z13: IHoge = func13(d);
const x14: number = func14(3); const y14: number = func14(d); const z14: IHoge = func14(d);
const x15: number = func15(3); const y15: number = func15(d); const z15: IHoge = func15(d);
まとめ
まとめますと、TypeScriptで関数の型定義は論理的には多種多様なパターンがありますが、個人的おすすめとして、
function シンボル(引数と型指定): 戻り値型指定 {
本体
}
か、
const シンボル: (引数と型指定) => 戻り値型指定 = (引数指定) => {
本体
}
が良いです。この2つで比べると、「=>」が出てこない上の方が一見簡単です。しかし「関数をかえす関数」の場合にはいずれにせよ「=>」を使わざるを得なくなります。
// 関数を返す関数
function シンボル(引数と型指定A): (引数と型指定B) => 戻り値型指定 {
return (引数指定B) => { 本体 }
}
// 関数を返す関数
const シンボル: (引数と型指定A) => (引数と型指定B) => 戻り値型指定
= (引数指定A) => {
return (引数指定B) => { 本体 }
}
これの各箇所で型指定を省略する、しないのパターンがあるので組み合わせ的に爆裂していき、ややこしです。
このルールをわかりやすく書きたかったのですが力およばず。読むポイントは、型アノテーションがどこか、型アノテーションはどこで終るか、です。
そして、型指定を、本体側でがんばるか、シンボル側でがんばるかですが、シンボル側に直接指定する型指定の部分をがんばって定義し、本体から暗黙の推論をさせないほうが、きっちりした型エラーチェックができると思います。特に、型が「関数を返す関数を…」といった複雑になる場合は、リターン型からの推論は、コードをまちがえたら終わりで、ある意味anyを使うのと同じだからです。
本体側からは型を省略していく方針もあれば、「重複をおそれずに」という方針もありえます。まあそこは適当に。