2022/11/08(火)TypeScriptでprocess.exit()ラッパーの後をデッドロジック扱いにする

更新日:
投稿日:

TSでprocess.exit()をラップしたカスタムexitを作った時に静的解析のフローが壊れたので、その対策。
因みにこの制御フローのことをControl Flow Analysisと呼ぶらしい。TS系の文書ではCFAと略されていることが多い模様。

問題事例

通常のprocess.exit()例の7-8行目のように次の行から先がデッドコードになって欲しいが、上手くいかないので、これを上手くいくようにする。

カスタムexitのコード例 通常のprocess.exit()例
カスタムexitのコード例
静的解析のエディタ表示例

確認環境

Env Ver
TypeScript 4.8.4

サンプルコード

オブジェクトでラップして、カスタムexit関数の戻り値をneverで指定してやると上手くいくようになる。
因みにこれTypeScriptの仕様らしく、上手くやる方法はあんまりなさそう。
実はアロー関数ではなくfunctionを使えば解決したりするが、それはなんか嫌なので…

example.ts

type ErrorPayload = {
  message: string;
  code: number;
};

type ExampleExit = {
  exit: (err: ErrorPayload) => never;
};

const exit = (err: ErrorPayload) => {
  process.exit(err.code);
};

export const Example: ExampleExit = { exit };

implements.ts

import { Example } from './example';

Example.exit({ message: 'exit', code: 1 });
console.log(1);

関連記事

2022/06/30(木)Next.js + Jestでコンポーネントの分岐出力結果をテストする

更新日:
投稿日:

条件分岐でコンポーネントの出し分けをしている時に、意図したとおりにコンポーネントが出ているかどうかをテストする方法。

UIロジックのリグレッションテストで使え、分岐結果の出力を見てるだけなのでテストとして壊れづらく、運用しやすいのではないかと考えている。

テスト対象の実装コード

JSXが分岐するコード

type BaseProps = {
  id: string;
};

type SwitchExampleProps = BaseProps & {
  display: 'Foo' | 'Bar';
};

export const Foo = (props: BaseProps) => {
  return (
    <div id={props.id}>
      <p>Foo</p>
    </div>
  );
};

export const Bar = (props: BaseProps) => {
  return (
    <div id={props.id}>
      <p>Bar</p>
    </div>
  );
};

export const SwitchExample = (props: SwitchExampleProps) => {
  if (props.display === 'Foo') {
    return <Foo id={props.id} />;
  } else {
    return <Bar id={props.id} />;
  }
};

2025年版のテストコード

TestRendererが非推奨になったため、その対応。

確認環境

Env Ver
@swc/core 1.11.13
@swc/jest 0.2.37
jest 29.7.0
next 15.2.4
react 18.3.1
react-dom 18.3.1
react-test-renderer 未使用
typescript 5.8.2

テストコード

コンポーネント呼び出しと、コンポーネントの中身べた書きの二つを書いて、その二つのHTMLが等価であるかどうかを見ている。

本稿ではコールバック関数の中身までは見ていないが、そちらはロジックテストでjest.fn()を差し込んで確認すればよいと思う。

import { render } from '@testing-library/react';
import { Bar, Foo, SwitchExample } from './SwitchExample';
import { JSX } from 'react';

type TestCase = {
  name: string;
  param: Parameters<typeof SwitchExample>[0];
  actual: JSX.Element;
};

describe('SwitchExample', () => {
  const testCaseItems: TestCase[] = [
    {
      name: 'Foo',
      param: {
        id: 'hoge',
        display: 'Foo'
      },
      actual: <Foo id={'hoge'} />
    },
    {
      name: 'Bar',
      param: {
        id: 'piyo',
        display: 'Bar'
      },
      actual: <Bar id={'piyo'} />
    }
  ];

  testCaseItems.forEach((item) => {
    it(`switched condition ${item.name}`, () => {
      const { container: result } = render(
        <SwitchExample id={item.param.id} display={item.param.display} />
      );
      const { container: actual } = render(item.actual);

      expect(result.innerHTML).toStrictEqual(actual.innerHTML);
    });
  });
});

2022年版のテストコード

確認環境

Env Ver
@swc/core 1.2.133
@swc/jest 0.2.17
jest 27.4.7
next 12.0.8
react 17.0.2
react-dom 17.0.2
react-test-renderer 17.0.2
typescript 4.5.4

テストコード

react-testing-libraryの.toHaveAttribute().toHaveDisplayValue()を書き連ねるより圧倒的に楽で保守性も良いと思う

import TestRenderer from 'react-test-renderer';
import { Bar, Foo, SwitchExample } from './SwitchExample';

type TestCase = {
  name: string;
  param: Parameters<typeof SwitchExample>[0];
  actual: JSX.Element;
};

describe('SwitchExample', () => {
  const testCaseItems: TestCase[] = [
    {
      name: 'Foo',
      param: {
        id: 'hoge',
        display: 'Foo',
      },
      actual: <Foo id={'hoge'} />,
    },
    {
      name: 'Bar',
      param: {
        id: 'piyo',
        display: 'Bar',
      },
      actual: <Bar id={'piyo'} />,
    },
  ];

  testCaseItems.forEach((item) => {
    // eslint-disable-next-line jest/valid-title
    it(`switched condition ${item.name}`, () => {
      const result = TestRenderer.create(
        <SwitchExample id={item.param.id} display={item.param.display} />
      );
      const actual = TestRenderer.create(<>{item.actual}</>);

      expect(result.toJSON()).toStrictEqual(actual.toJSON());
    });
  });
});

参考記事

2021/11/08(月)nginxでNode.jsインスタンスに対してリバースプロキシする

更新日:
投稿日:

Proxy headerを設定しないとiOS Safari環境で上手くアクセスできないケースがあるのでやる設定

確認環境

Env Ver
nginx 1.19.8

サンプルコード

httpセクション

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

リバースプロキシ設定

location @prox {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass  https://example.com;
}

2021/10/06(水)Next.js + TypeScriptでuseRouter()をモックする

更新日:
投稿日:

確認環境

Env Ver
next 11.1.2
typescript 4.3.4

サンプルコード

__mocks__/next/router.ts

export const useRouter = () => {
  return {
    route: '/',
    pathname: '',
    query: '',
    asPath: '',
    push: jest.fn(),
    replace: jest.fn(),
    events: {
      on: jest.fn(),
      off: jest.fn(),
    },
    beforePopState: jest.fn(() => null),
    prefetch: jest.fn(() => null),
  };
};

2021/07/04(日)Babelとtscの比較

更新日:
投稿日:

Babelとtscどっちがいいのか気になったので調べて見たメモ
結論から言うと基本的にはtscで良い

確認環境

Env Ver
@babel/cli 7.14.5
@babel/core 7.14.6
@babel/preset-env 7.14.7
@babel/preset-typescript 7.14.5
typescript 4.3.2

Babelとは何か?

What is Babel? よりBabelとはJavaScriptのコンパイラと説明されている

Babelがしてくれること

  • 構文の変換
  • corejsを使ったPolyfill
  • ソースコードの変換(codemods
  • その他色々

基本的にはES6+をES6にしてくれると考えれば良さそうです
でもそれって別にtscでもいいいよねって思う

Babelの導入

基本はこれ

npm i -D @babel/core @babel/cli @babel/preset-env

Babelの設定

.babelrcを作ってその中にJSONを書いていく
こんな感じ

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "ie": "11",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Babelの機能について

コンポーネント

@babel/cli

BabelのCLI、これがないと始まらない

@babel/core

Babel本体、CLIがよしなにしてくれる

@babel/preset-env

構文の変換やPolyfillを設定できる

@babel/preset-typescript

TSをトランスパイルしてくれる

BabelとBrowserlist

BabelはBrowserlistの設定を認識して自動でPolyfillを挿入してくれます
なお、Babel 7.4.0以前では設定方法が異なる可能性があります

サンプルコード

IE11をターゲットにした設定のサンプルです
以下のコマンドを流せばPolyfillされたJSが出ることを確認できます
npx babel src -d dest --extensions ".ts"

.babelrc
{
  "presets": [
    ["@babel/preset-env", { "corejs": 3, "useBuiltIns": "usage" }],
    ["@babel/preset-typescript"]
  ]
}
.browserlist
ie 11

Babelとtscでトランスパイルしてみる

割と違うコードが出てきます

元のソース

const sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
const prm = new Promise((res) => res(true));
console.log(prm);
[...Array(10)].forEach((_, i) => console.log(i));

console.log(globalThis.Date());

export {};

Babel

.babelrc
{
  "presets": [["@babel/preset-env"], ["@babel/preset-typescript"]]
}
ビルド結果
"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true,
});

function _toConsumableArray(arr) {
    return (
        _arrayWithoutHoles(arr) ||
        _iterableToArray(arr) ||
        _unsupportedIterableToArray(arr) ||
        _nonIterableSpread()
    );
}

function _nonIterableSpread() {
    throw new TypeError(
        "Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
    );
}

function _unsupportedIterableToArray(o, minLen) {
    if (!o) return;
    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
    var n = Object.prototype.toString.call(o).slice(8, -1);
    if (n === "Object" && o.constructor) n = o.constructor.name;
    if (n === "Map" || n === "Set") return Array.from(o);
    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
        return _arrayLikeToArray(o, minLen);
}

function _iterableToArray(iter) {
    if (
        (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) ||
        iter["@@iterator"] != null
    )
        return Array.from(iter);
}

function _arrayWithoutHoles(arr) {
    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}

function _arrayLikeToArray(arr, len) {
    if (len == null || len > arr.length) len = arr.length;
    for (var i = 0, arr2 = new Array(len); i < len; i++) {
        arr2[i] = arr[i];
    }
    return arr2;
}

var sp = new URLSearchParams("?aaa=bbb&ccc");
console.log(sp);
var prm = new Promise(function (res) {
    return res(true);
});
console.log(prm);

_toConsumableArray(Array(10)).forEach(function (_, i) {
    return console.log(i);
});

console.log(globalThis.Date());

tsc

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": true,
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "isolatedModules": true,
    "baseUrl": ".",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}
ビルド結果
"use strict";
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
    for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
        to[j] = from[i];
    return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
var sp = new URLSearchParams('?aaa=bbb&ccc');
console.log(sp);
var prm = new Promise(function (res) { return res(true); });
console.log(prm);
__spreadArray([], Array(10)).forEach(function (_, i) { return console.log(i); });
console.log(globalThis.Date());
//# sourceMappingURL=index.js.map

結論

tscで問題ない

tscの方が綺麗なコードが出てきてるので、Polyfill要らなければtscで問題ないです
因みにES6+をES5にするのもできるのでJSのトランスパイルにも使えます

core-jsを使うならBabel

ちょいちょい触っててBabelの利点はcore-jsがあれば.browserlistを使えるので、そこでターゲットを指定してやればPolyfillを勝手に差し込んでくれるところですね

但しCRAではBabelに対する.browserlistはほぼ無価値

あとはCreate React AppはビルドにBabelを採用しているので、FW側でビルドパイプラインがあるときには採用したほうが楽です
(態々書き換える意味もないので)
ただreact-scripts 4.0.3にはcore-jsが入っていないので、基本的に.browserlistを書いたところでPolyfillは入らないため、あんまり存在感はないです