LoginSignup
0
0

TypeScriptでオブジェクトかクラスを引数にとる場合のオーバーロード

Posted at

オブジェクトもしくはクラスを引数に取る関数を作成したくなることはないですか?私はあります!

function create<T extends object>(spec: T | (new () => T)): T {
    return typeof spec === 'function' ? new spec() : spec;
}

こんな感じでクラスが指定されていればnewしてインスタンスを作成し、オブジェクトが指定されていればそのまま使う、というようなものを想定しています。

この関数を使うと

interface Store {
    name: string;
    onChangeName(newName: string): void;
}

const initial = {
    name: '',
    onChangeName(newName: string) {
        this.name = newName;
    },
};
const f1 = (): Store => {
    return create(initial);
};

class StoreSample {
    name = '';
    onChangeName(newName: string) {
        this.name = newName;
    }
}
const f2 = (): Store => {
    return create(StoreSample);
};

のようにオブジェクトでもクラスでも引数に指定すればよしなにインスタンスを生成してくれます。

無名クラスだってこの通り。

const f3 = (): Store => {
    return create(
        class {
            name = '';
            onChangeName(newName: string) {
                this.name = newName;
            }
        },
    );
};

オブジェクトリテラルだって。

const f4 = (): Store => {
    return create({
        name: '',
        onChangeName(newName: string) {
            this.name = newName;
        },
    });
};

ん?

image.png

何かエラーが出てますね…

image.png

thisがUnion型になってる!!

クラスが指定されていることになってる?でもオブジェクトリテラルなのに…

呼び出しているcreateのシグネチャがUnion型のせいでしょうか?

とりあえずオーバーロード宣言をして回避してみます。

function create<T extends object>(spec: new () => T): T;
function create<T extends object>(spec: T): T;
function create<T extends object>(spec: T | (new () => T)): T {
    return typeof spec === 'function' ? new spec() : spec;
}

これでエラーは出なくなりました。 😌

さて次にこの関数と同じ引数を取り、この関数を呼び出して手を加えたものを返す関数を作成します。

function setup<T extends object>(spec: new () => T): T;
function setup<T extends object>(spec: T): T;
function setup<T extends object>(spec: T | (new () => T)): T {
    return Object.defineProperty(create(spec), immerable, {
        value: true,
    });
}

二度も同じ失敗はしませんよ。同じようにオーバーロードの宣言を付けてcreateを呼び出して…おや?

image.png

またUnion型になってる!

setupの実装の中ではspecがUnion型のままなので、createにもUnion型のまま受け取れるようにオーバーロードを追加しないとダメなのかも知れません。

function create<T extends object>(spec: new () => T): T;
function create<T extends object>(spec: T): T;
function create<T extends object>(spec: T | (new () => T)): T;
function create<T extends object>(spec: T | (new () => T)): T {
    return typeof spec === 'function' ? new spec() : spec;
}

これで…あれぇ?

image.png

同じエラーのままですね…

実はこれ、2番目のオーバーロードが何でも受け付けちゃう感じになっているので、そちらに当てはめられてしまっているのでした。

今回の場合オブジェクトのときだけ当てはまってくれればいいので
Excludeでクラスが指定されている場合を除外するようにしてみます。

function create<T extends object>(spec: new () => T): T;
function create<T extends object>(spec: Exclude<T, new () => unknwon>): T;
function create<T extends object>(spec: T | (new () => T)): T;
function create<T extends object>(spec: T | (new () => T)): T {
    return typeof spec === 'function' ? new spec() : spec;
}

すると

image.png

エラーがなくなりました。

そのままだと冗長なので整理すると以下になります。

function create<T extends object>(spec: Exclude<T, Function>): T;
function create<T extends object>(spec: T | (new () => T)): T;
function create<T extends object>(spec: T | (new () => T)): T {
    return typeof spec === 'function' ? new spec() : spec;
}

オブジェクトとクラスの両方を受け取れる関数を作るときはこんな感じでオーバーロード宣言する必要があるようです。

順番も重要ですので、この順番を守って下さいね。

※アドカレに参加しようと思ってたのに忘れてた…

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0