vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

TypeScript で関数オーバーロード

はじめに

C# だと時々使う関数のオーバーロード

TypeScript で同じように書くとエラーになるため、使えないのかな~と思っていたのですが、よくよく調べてみると使えるらしい。しかしながらちょっと様子が違う?となったので、あれこれ試してみることにしました。

オーバーロード

まず C#オーバーロードから。

Sample.cs

public class Sample
{
    public string GetMessage(string message)
    {
        return "hello";
    }
    public int GetMessage(int message)
    {
        return 200;
    }
}

各メソッドはメソッド名と引数の型、数で区別され、別物として扱われます。

戻り値のみ異なる場合はエラーになります。

このコードをそのまま TypeScript で書いてみると。。。

Sample.ts

class Sample{
    public getMessage(message: string): string{
        return message;
    }
    
    // エラーとなる.
    public getMessage(message: number): number{
        return message;
    }
    
}

Duplicate function implementation と怒られてしまいます。

C# に合わせてクラスのメソッドを見ていますが、関数の場合も同じです。

理由は返還後の JavaScript を見ればすぐにわかります。

Sample.js

class Sample {
    getMessage(message) {
        return message;
    }
    getMessage(message) {
        return message;
    }
}

型の情報が消えてしまうため、まったく同じメソッドになってしまう、というわけですね。

ただ、引数の数を変更しても同じようにエラーとなるのは気になるところですが。。。

プロパティなどと同様、動的に変わるものということで、メソッドを判別する要素として使用していない、ということなのでしょう。

とはいえ TypeScript にもオーバーロードはあります

では TypeScript にはオーバーロードはないのか?というと、そうではありません。

Sample.ts

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: any): any{
        return message;
    }
}

※ ググった内容から、昔は戻り値が同じ型である必要があったようですが、少なくとも ver.3.6 では異なっていても問題ありません。

最初見たときちょっと驚きましたが、重要なのは実際の処理を行う引数・戻り値が any のメソッドです。

今回はオーバーロードしているメソッドの戻り値が string と number であるため any としていますが、下記のように、オーバーロードしている全メソッドの戻り値の型の親となる型であれば OK です。

Sample.ts

class Sample{
    public getMessage(message: string): "hello";
    public getMessage(message: number): "world";
    // 戻り値は string で OK
    public getMessage(message: any): string{
        return message;
    }
}

なお、 JavaScript に変換した結果は下記の通りです。

Sample.js

class Sample {
    getMessage(message) {
        return message;
    }
}

実際の処理を書いているメソッド以外のオーバーロードの内容はすべて消されてしまうわけですね。

引数の型によって処理を変えたいときもあるよね

やったね TypeScript でもオーバーロードができた!

。。。と言いたいところですが、これだと型が違っても全く同じ処理をすることしかできません。 (全く違うことをするのであれば別のメソッド・関数にすべきだとは思いますが)

となると、いったん引数を any で受け取って、その型を調べて処理を分ける、という方法をとる必要がありそうです。

型を調べる

引数の型に合わせて処理を分けるには、当然ながらその引数の値の型を調べる必要があります。

typeof

string や oject などの基本型は、 typeof(引数) で調べることができます。

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • object
  • function

Sample.ts

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: any): any{
        if(typeof(message) === "string"){
            console.log("message type is string: " + message);
        }
        if(typeof(message) === "number"){
            console.log("message type is number: " + message);
        }
        return message;
    }
}
let s = new Sample();
s.getMessage("hello");
s.getMessage(30);

出力結果は下記の通りです。

message type is string: hello
message type is number: 30

自作の型・interface・クラス・関数

自作の型・interface・クラス・関数を調べたい場合はどうすればよいでしょうか。

typeof の結果は object となります。

Sample.ts

let s = new Sample();
// object と出力される
console.log(typeof(s));
// コンパイルエラー
console.log(typeof(s) === "Sample");

というわけで、 typeof を使用することはできません。

ではどうするか。

上記ドキュメントによると、このようにして比較するようです。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class Sample{
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).id !== undefined;
    }
}

えぇ。。。

as

ちなみに message as TypeSample だけじゃダメなの?というと。。。

let id = 30;
let t = (id as any as TypeSample);
console.log(t);

一旦 any にキャストしているのは number のままだとコンパイルエラーになるためです。

この出力結果は下記の通り。

30

えぇ。。。 undefined とかじゃないの。。。

実は JavaScript に変換すると as ~ が削除されるため、下記のようになります。

let id = 30;
let t = id;
console.log(t);

そりゃ undefined にはならないですよね~/(^o^)\

あと、引数が指定の型( TypeSample )である、という表現が、 message is TypeSample となるのは面白いですね :)

プロパティ選びは慎重に

この判別方法の課題は、同じプロパティを持っていると区別できない、という点です。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class Sample{
    public getMessage(message: TypeSample): TypeSample;
    public getMessage(message: Function): Function;
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).name != null;
    }
}
let s = new Sample();

// 1. TypeSample を渡した場合.
s.getMessage({
    id: 20,
    name: "たこ焼き"
});

// 2. Function を渡した場合.
s.getMessage(() => console.log("チーズ"));

この実行結果は両方とも true になります。

これは、関数がデフォルトで name というプロパティを持っているためです。

とすると、

Sample.ts

type TypeSample = {
    id: number,
    name: string,
    type: "TypeSample"
};
const TypeNameTypeSample: "TypeSample" = "TypeSample";
class Sample{
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).type === TypeNameTypeSample;
    }
}
let s = new Sample();

s.getMessage({
    id: 20,
    name: "たこ焼き",
    type: "TypeSample"
});
s.getMessage(() => console.log("チーズ"));

。。。なんだろうこの敗北感。

まぁ string 型にするよりはマシとはいえ、面倒くさいし間違いやすいし。

そもそも判別しうるのか

たとえば type のインスタンスを作って中身を覗いてみると。。。

let t: TypeSample = {
    id: 20,
    name: "たこ焼き"
};
console.log(t);

中身はこのようなものになります。

{…}
    id: 20
    name: "たこ焼き"
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

はい、「 TypeSample 」という型情報がありませんね。

type 、 interface は、例えばあるクラスインスタンスが同じプロパティを持っていれば、その type 、 interface を実装していると見なされます。

そのため、 TypeSample 型であるか、ということは、プロパティからしか判別できないと。

もしプロパティ一つだと判別できない、という話であれば、全プロパティを確認しても良いかもしれません(あまりそのシチュエーションはなさそうですが)。

Sample.ts

~省略~
const TypeSampleChecker: TypeSample = {
    id: -1,
    name: ""
};
class Sample{
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        // TypeSample の全プロパティをチェック.
        for(let p in TypeSampleChecker){
            if(message[p] == null){
                return false;
            }
        }
        return true;
    }
}
let s = new Sample();
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
s.getMessage(() => console.log("チーズ"));

ではクラスインスタンスはどうか

ではクラスインスタンスの場合はどうかというと、 instanceof で判定できそうです。

Sample.ts

~省略~
class ClassSample{
    public id: number = 99;
    public name: string = "めんたい";
}
class Sample{
    public getMessage(message: ClassSample): ClassSample;
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isClass(message));
    }
    ~省略~
    private isClass(message: any): message is ClassSample{
        return message instanceof ClassSample;
    }
}
let s = new Sample();
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
s.getMessage(() => console.log("チーズ"));
let f = () => console.log("コーンポタージュ");

s.getMessage(new ClassSample());

結果は下記の通り。

false
false
true

良いですね :)

ClassSample を継承した場合はどうでしょうか。

Sample.ts

~省略~
class ExtendClass extends ClassSample{
}
s.getMessage(new ExtendClass());

これも true になりました。

もし継承したクラスを除外したい場合、 message.constructor.name でクラス名を取ると良いかもしれません。

念のためプロパティが同じ別のクラスも見ておきます。

Sample.ts

~省略~
class OtherClass{
    public id: number = 99;
    public name: string = "めんたい";
}
s.getMessage(new OtherClass());

結果は false になります。

結果自体は良いのですが、メソッド(関数)の引数としては、クラスが異なっても同じプロパティを持っていれば渡すことができてしまうのですね。

地味に驚きました。

まとめ

これらを組み合わせると、オーバーロードしたメソッドの処理を引数の型に合わせて切り替えられそうです。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class ClassSample{
    public id: number = 99;
    public name: string = "めんたい";
}
const TypeSampleChecker: TypeSample = {
    id: -1,
    name: ""
};
const ClassSampleChecker = new ClassSample();

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: ClassSample): ClassSample;
    public getMessage(message: TypeSample): TypeSample;
    public getMessage(message: Function): Function;
    public getMessage(message: any): any{
        switch(typeof(message)){
            case "string":
                console.log("[type:string] " + message);
                break;
            case "number":
                console.log("[type:number] " + message);
                break;
            case "function":
                console.log("[type:function] " + message);
                break;
            case "object":
                // type、classの判別.
                if(this.isClass(message)){
                    console.log("[type:ClassSample] " + message);
                }
                else if(this.isType(message)){
                    console.log("[type:TypeSample] " + message);
                }
                else{
                    console.error("type not found");
                }
                break;
            default:
                console.error("type not found");
                break;
        }
        return message;
    }
    private isType(message: any): message is TypeSample{
        // TypeSample の全プロパティをチェック.
        for(let p in TypeSampleChecker){
            if(message[p] == null){
                return false;
            }
        }
        return true;
    }
    private isClass(message: any): message is ClassSample{
        return message instanceof ClassSample;
    }
}
let s = new Sample();
// string
s.getMessage("サラミ");
// number
s.getMessage(97);
// ClassSample
s.getMessage(new ClassSample());
// TypeSample
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
// Function
s.getMessage(() => console.log("チーズ"));

結果は下記の通り。

[type:string] サラミ
[type:number] 97
[type:ClassSample] [object Object]
[type:TypeSample] [object Object]
[type:function] () => console.log("チーズ")

。。。動作には問題がないのですが、どうでしょうこの溢れ出る別メソッドに分けろよ感は。

ご利用は計画的に( ˘ω˘ )