関数オーバーロード

Functions · TypeScript

上記URLの最後、関数オーバーロード(記事のOverloads項目)なる言語仕様について。まずは以下のようなのがあったとします。

// .tsxファイル まずはこんなのがあったとします。
import * as React from 'react';

type P = { message: string };
type PP = { greeting: string };

function hoc(Comp: React.ComponentType<Partial<P>>): React.ComponentClass<PP> {
    return class extends React.Component<PP> {
        render () {
            return <Comp message={this.props.greeting} />;
        }
    };
}

たった一箇所のJSXタグのために.tsxにするが、それもまたよし。しかしなんか美しくない気がしてReact.createElementを使って書き直したかったけどはじめは理屈がわからなかったので難しかった。言語仕様を勉強してからcreateElementを見直し、簡単に例えると以下のようになっていたことがわかりました。

test('Function Overload', () => {
    function hoge(arg: number): number; // 後方の緩い関数に型の縛りをつけたエリアス、その1
    function hoge(arg: string): string; // 後方の緩い関数に型の縛りをつけたエリアス、その2
    function hoge(arg: any): any { // <- 関数の実体。anyで受けてanyで返す。でも型が緩くてイヤ
        if (typeof arg === 'number') {
            return arg * arg;
        } else if (typeof arg === 'string') {
            return arg + ', Hi!';
        }
        return false;
    }
    expect(hoge(5)).toEqual(25);
    expect(hoge('Masataka')).toBe('Masataka, Hi!');
});

hoge関数はnumberを引数にとるとnumberを、stringを引数にとるとstringを返す関数です。同じようにcreateElementは、StatelessComponentを引数にとるとSFCElement、ComponentClassを引数にとるとCElementを返してました。JavaScriptだとプリミティブ型だけで実装型がないから気にせずなんでもできるけど、TypeScriptではそのままだと型が緩くなってイヤ。なので関数の実体は一つだけど、エリアスをつけて型の縛りだけやっときましょうというもの。

React.ComponentTypeという型が、React.StatelessComponent | React.ComponentClassというユニオン型で、一度引数に渡されてきてこの型になっちゃったら、React.ComponentClassとReact.StatelessComponentのどちらの単体にもそのままでは再キャストすることはできない。

// .tsファイル
import {
    Component, ComponentClass, StatelessComponent, ComponentType, createElement,
} from 'react';

type P = { message: string };
type PP = { greeting: string };

function hoc(Comp: ComponentType<Partial<P>>): ComponentClass<PP> {
    return class extends Component<PP> {
        render () {
            const props = { message: this.props.greeting };
            // ComponentClassかStatelessComponentのどちらかにas演算
            // 関数オーバーロードされているし、実行時はTypeScript型はなくなるのでOK
            return createElement<P>(Comp as ComponentClass<P>, props);
        }
    };
}

as演算は型キャストとちがって型チェックエラーが発生しないので役立ちます。他の実装系での同様機能は型があわない場合はコンパイラエラーだったりnullに変換されたりしますが、TypeScriptは実行時にはECMAScriptにトランスパイルされて実装型がなくなるので余計なことは起きない。ただそこにある値の正しい扱いか間違いなのか関係なくコードが書けるだけ。操作によって実行時エラーが起きるか起きないかは実装内容次第。

  • 引数によって戻値内容が変化する関数をTypeScriptで型表現するには関数オーバーロード
  • ユニオン型は、その構成要素の互換性のない単体型それぞれにキャストすることができない
  • ユニオン型もしくはanyで引数を受けたら、内部ではas演算で型をあわせると辻褄があい、実行時はそもそも無問題

結論

ほかにもユニオン型で書いて怒られても理由がわからず、結局anyに緩くしちゃってたところの解決方法が副作用として理解できました。なるほどユニオン型を構成要素にはキャストできないのか。わかってみるとそりゃそうですね、だからユニオン型で書くのだし。見よう見まねでasをよくわからず書いてたところも、その意味が理解できた。型定義ファイルで大量に同じ名前のものが並んでるのも納得できる。

TypeScriptのユニオン型と関数オーバーロードとas演算子をセットで理解したら、一段上がってかなり目が開けたような気がします。理解できない型チェックエラーが劇的に減りました。