- 投稿日:
TSでprocess.exit()
をラップしたカスタムexitを作った時に静的解析のフローが壊れたので、その対策。
因みにこの制御フローのことをControl Flow Analysisと呼ぶらしい。TS系の文書ではCFAと略されていることが多い模様。
問題事例
通常のprocess.exit()
例の7-8行目のように次の行から先がデッドコードになって欲しいが、上手くいかないので、これを上手くいくようにする。
カスタムexitのコード例 | 通常のprocess.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);
関連記事
- TypeScriptで戻り値型がneverな関数を作る
- オブジェクトに包まれていない、裸の関数に対して同様のことを行うアプローチを書いている
- 投稿日:
条件分岐でコンポーネントの出し分けをしている時に、意図したとおりにコンポーネントが出ているかどうかをテストする方法。
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());
});
});
});
参考記事
- 投稿日:
TypeScriptをぼちぼちやってきた中で、型推論が素直に決まらないみたいな所が出てきたので書き置きとして残しておきます
具体的には何かしらの関数を通して処理を隠蔽すると上手くいかなくなるケースがあります
型が推論されないケース
検査関数的なものを通した場合に、後続処理で期待通り型が推論されなくなるケースです
その1
type HogePiyo = 'hoge' | 'piyo';
type InputProps<ValueT> = {
value: ValueT;
error: string;
};
const required = (val: string) => {
return val === '';
};
const registerHogePiyo = (value: HogePiyo) => {
console.log('register', value);
};
const input: InputProps<HogePiyo | ''> = {
value: '',
error: '',
};
if (required(input.value)) {
input.error = 'ERR';
console.error(input.error);
} else {
// ここでは input.value は 'hoge' | 'piyo' となるはずだが
// TSC は 'hoge' | 'piyo' | '' と解釈する
registerHogePiyo(input.value);
}
その 2
type HogePiyo = 'hoge' | 'piyo';
type InputProps<ValueT> = {
value: ValueT;
error: string;
hasError: boolean;
};
const required = <ValueT>(obj: InputProps<ValueT | ''>) => {
if (obj.value === '') {
return { value: '', error: '必須入力です', hasError: true };
} else {
return { value: obj.value, error: '', hasError: false };
}
};
const registerHogePiyo = (value: HogePiyo) => {
console.log('register', value);
};
const input: InputProps<HogePiyo | ''> = {
value: '',
error: '',
hasError: false,
};
const result = required(input);
if (result.hasError) {
console.error(result.error);
} else {
// ここでは input.value は 'hoge' | 'piyo' となるはずだが
// TSC は 'hoge' | 'piyo' | '' と解釈する
registerHogePiyo(result.value);
}
export {};
型を推論されるようにしたケース
オブジェクトに型判定用のプロパティを生やしas
で型を明示することでクリアしています
type HogePiyo = 'hoge' | 'piyo';
type InputProps<ValueT> = {
value: ValueT;
error: string;
hasError: boolean;
};
const required = <ValueT>(obj: InputProps<ValueT | ''>) => {
if (obj.value === '') {
return { value: '', error: '必須入力です', hasError: true } as {
value: '';
error: string;
hasError: true;
};
} else {
return { value: obj.value, error: '', hasError: false } as {
value: ValueT;
error: string;
hasError: false;
};
}
};
const registerHogePiyo = (value: HogePiyo) => {
console.log('register', value);
};
const input: InputProps<HogePiyo | ''> = {
value: '',
error: '',
hasError: false,
};
const result = required(input);
if (result.hasError) {
console.error(input.error);
} else {
registerHogePiyo(result.value);
}
export {};
あとがき
- Assertion Functionsを使う方法もあると思いますが、何も考えずAssertion Functionsを書くと次のような弊害があり、積極的に採用しづらいと考えています
- 何もしていなければ基本的にバンドル後のソースコードに出てくる
- 要するにバンドルサイズが大きくなります
throw
を書く必要性があり、バンドル後に残っている以上、このthrow
をcatch
する必要性がある- 無意味な実装工数が生まれる
- そもそも論としてAssertion Functionsを書く工数が生まれる
- Assertion Functionsの振る舞いを見ている限り、
as
に限りなく近く、ぶっちゃけas
で書いたほうが楽
- 何もしていなければ基本的にバンドル後のソースコードに出てくる
- 絶対に
as
を使わず現実的な工数で開発するというのは、割と非現実ではないかという感触があります- 個人的に型推論はIDEが勝手にやってくれるおかげで楽ができるシステムだと考えているので、型推論の補佐をする仕組みに工数をかけるというのは本末転倒な気がしています
- 品質は大切なことですが、反面で少なくないビジネスには期限もあります
- 品質に寄せすぎた過剰に冗長なコードも、納期に寄せすぎた脆弱性不具合山盛りスパゲティコードもどちらも考えものであり、その辺のバランスが上手くとれるやり方がなんかないかなーと考えることはしばしばあります