2022/11/08(火)TypeScriptでprocess.exit()ラッパーの後をデッドロジック扱いにする
投稿日:
TSでprocess.exit()をラップしたカスタムexitを作った時に静的解析のフローが壊れたので、その対策。
因みにこの制御フローのことをControl Flow Analysisと呼ぶらしい。TS系の文書ではCFAと略されていることが多い模様。
問題事例
通常のprocess.exit()例の7-8行目のように次の行から先がデッドコードになって欲しいが、上手くいかないので、これを上手くいくようにする。
確認環境
| 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);
関連記事
- TypeScriptで戻り値型がneverな関数を作る
- オブジェクトに包まれていない、裸の関数に対して同様のことを行うアプローチを書いている
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は入らないため、あんまり存在感はないです

