🐥

TypeScriptでswitchをオブジェクトで置き換える

2022/06/29に公開
2

JavaScript(TypeScript)のswitch文をオブジェクトで書き換えることで、コードをより簡潔に書ける場合があります。

switch文をオブジェクトリテラルに変換する簡単な例

例えば処理させたい情報を持ったtodoと、todoの処理内容を記述した関数(foo,barなど)があったとします。

これを愚直にswitchを使って書くと以下のようになります。

const foo = () => {
  console.log('doing foo...');
};

const bar = () => {
  console.log('doing bar...');
};

const todo = ['foo', 'bar', 'hoge', 'toString', 'constructor'];

// switch version
todo.map((value) => {
  switch (value) {
    case 'foo':
      foo();
      break;

    case 'bar':
      bar();
      break;

    default:
      console.log(`${value} is not supported`);
      break;
  }
});

このコードの出力結果は以下のようになります。

doing foo...
doing bar...
hoge is not supported
toString is not supported
constructor is not supported

ただし、このコードには以下の問題点があります。

  1. 対応するメソッドを編集する度にswitch文の中身も編集しなければならない。
  2. break;を忘れがち

そこで以下のようにオブジェクトを使って書き直すことによって、switch文が抱える問題を解消できます。

// object literal version
const switcher = {
  foo,
  bar,
};

todo.map((value) => {
  Object.hasOwn(switcher, value)
    ? switcher[value as keyof typeof switcher]()
    : console.log(`${value} is not supported`);
});

この書き方であれば、メソッドを追加する時はswitcherオブジェクトに追加するだけで良くなります。

オブジェクトリテラルによるswitchの応用例

例えばクローラーを作っていて、URLごとに処理を変更したい場合などに、このテクニックが使えます。
new URL()を使ってURLをパースし、URL.hostnameでドメインごとに動作を切り替えます。ただし、サイトによってはPC版とモバイル版でサブドメインが異なっている場合があるため、単にhostnameを使っても思うように切り替えできません。例えばYoutubeは同じ動画に対して4種類以上ものURLが存在します。

'https://www.youtube.com/watch?v=bQqN0fK3Gjg', // PC版YoutueのURL
'https://youtu.be/bQqN0fK3Gjg', // PC版Youtubeで生成した共有用のURL
'https://m.youtube.com/watch?v=bQqN0fK3Gjg', // Mobile版YoutubeのURL
'https://www.youtube.com/watch?app=desktop&v=bQqN0fK3Gjg', // Mobile版YoutubeのURLをPCで開いた時のURL

そこでpsl (Public Suffix List)というパッケージを使って、サブドメインを削除したドメインを取得します。

npm i psl
npm i --save-dev @types/psl

psl.get()を使うとサブドメインを削除したドメインを取得できます。

import psl from 'psl';

const Urls = [
  'https://www.youtube.com/watch?v=bQqN0fK3Gjg', // PC版YoutueのURL
  'https://youtu.be/bQqN0fK3Gjg', // PC版Youtubeで生成した共有用のURL
  'https://m.youtube.com/watch?v=bQqN0fK3Gjg', // Mobile版YoutubeのURL
  'https://www.youtube.com/watch?app=desktop&v=bQqN0fK3Gjg', // Mobile版YoutubeのURLをPCで開いた時のURL
];

Urls.map((url) => {
  const host = new URL(url).hostname;
  const domain = psl.get(host);
  console.log(domain);
});

以下のような出力が得られます。

youtube.com
youtu.be
youtube.com
youtube.com

残念ながら、youtu.beドメインに関してはyoutube.comとして変換してくれませんが、wwwmなどのサブドメインを含んでいるURLからyoutube.comを抽出することに成功しました。

以下のコードはURLのリストをサイトごとに振り分けるコードの例です。switchの代わりにオブジェクトリテラルを使うことで、コードの見通しやメンテナンス性が上がったことを実感していただければ幸いです。

import psl from 'psl';

const Urls = [
  'https://www.youtube.com/watch?v=bQqN0fK3Gjg', // PC版YoutueのURL
  'https://youtu.be/bQqN0fK3Gjg', // PC版Youtubeで生成した共有用のURL
  'https://m.youtube.com/watch?v=bQqN0fK3Gjg', // Mobile版YoutubeのURL
  'https://www.youtube.com/watch?app=desktop&v=bQqN0fK3Gjg', // Mobile版YoutubeのURLをPCで開いた時のURL
  'https://twitter.com/vercel/status/1541814188619825154', // TwitterのURL
  'https://mobile.twitter.com/vercel/status/1541814188619825154', // Mobile版のTwitterのURL
];

const youtubeParser = (url: string) => {
  console.log(`Parsing ${url}`);
};

const twitterParser = (url: string) => {
  console.log(`Parsing ${url}`);
};

const parserSwitcher = {
  'youtube.com': youtubeParser,
  'youtu.be': youtubeParser,
  'twitter.com': twitterParser,
};

Urls.map((url) => {
  const host = new URL(url).hostname;
  const domain = psl.get(host);

  domain && Object.hasOwn(parserSwitcher, domain)
    ? parserSwitcher[domain as keyof typeof parserSwitcher](url)
    : console.log(`${url} is not supported`);
});

Discussion

standard softwarestandard software

以前にこのようなコードに関しての記事を書いたことがあります。

オブジェクトを連想配列として使うと痛い目にあいますよ
https://zenn.dev/standard_soft/articles/7458d1f49fd2ef

psl というものについては使ったことがないので検証できてないのですが、もしかしたらUrlsに意図せずtoStringとかの文字列が入ったとき、意図しない動きになるかもしれません。
(parserSwitcherに関数登録されてないからエラーになるのかな)

コード読むだけでは不具合があるのかないのかも見抜く事が難しいので
「コードの見通しやメンテナンス性が上がった」というのではなく、
逆にswitch文で書いているほうが、コードの見通しもメンテナンス性も高いように思います。

gladevisegladevise

確かにinを使うと継承元のプロパティまで遡ってしまうので、おかしな挙動になってしまいます。
Object.hasOwn()を使って書き直しました。
ご指摘いただきありがとうござます!!!