オブジェクトもしくはクラスを引数に取る関数を作成したくなることはないですか?私はあります!
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;
},
});
};
ん?
何かエラーが出てますね…
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
を呼び出して…おや?
また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;
}
これで…あれぇ?
同じエラーのままですね…
実はこれ、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;
}
すると
エラーがなくなりました。
そのままだと冗長なので整理すると以下になります。
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;
}
オブジェクトとクラスの両方を受け取れる関数を作るときはこんな感じでオーバーロード宣言する必要があるようです。
順番も重要ですので、この順番を守って下さいね。
※アドカレに参加しようと思ってたのに忘れてた…