環境構築でハマったこと・実装アンチパターン等のメモ
後で何のためにやった設定か分からなくなることがある(特に「あえてこうしていない」系のもの)ので作業履歴として諸々残していくことにした。
TypeScript v5 にアップデートして tsconfig.json の extends に配列を用いるようにしたら cypress run 実行時に
DevTools listening on ws://127.0.0.1:36551/devtools/browser/db3972a0-fbb9-4e6a-9fe0-4692cf1abf05
[8956:0115/181526.210849:ERROR:object_proxy.cc(590)] Failed to call method: org.freedesktop.portal.Settings.Read: object_path= /org/freedesktop/portal/desktop: unknown error type:
node:internal/process/esm_loader:34
internalBinding('errors').triggerUncaughtException(
^
TypeError [Error]: value.replace is not a function
at normalizeSlashes (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/util.js:62:18)
at Object.getExtendsConfigPath (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/ts-internals.js:24:54)
at readConfig (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/configuration.js:127:64)
at findAndReadConfig (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/configuration.js:50:84)
at create (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/index.js:146:69)
at register (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/index.js:127:19)
at Object.registerAndCreateEsmHooks (/home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/dist/esm.js:25:49)
at file:///home/xxx/.cache/Cypress/13.6.2/Cypress/resources/app/node_modules/ts-node/esm/transpile-only.mjs:8:7
at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
というエラーが出るようになってしまった。
tsconfig.json のパスを指定するオプションが分からなかったので、 cypress 実行ディレクトリをネストさせて tsconfig.json を分離するようにして回避した。
(間違ってコメントした issue : https://github.com/cypress-io/cypress/issues/28061#issuecomment-1891652364 )
workspace のルートの型チェック用 tsconfig.json に cypress ディレクトリを含めたら jest の型定義と cypress の型定義が conflict することが分かったため、 cypress 用の type-check 用 tsconfig と eslint.config.js は分けるようにした。
cypress ディレクトリをネストさせたら cypress-fail-on-console-error が実行時エラーを吐くようになった。
Error: Webpack Compilation Error
Module not found: Error: Can't resolve 'process/browser' in '/home/xxx/repo/node_modules/sinon/pkg'
Cypress は特定のディレクトリ構造に強く依存している傾向があるっぽくて気持ち悪いが、今回は他の選択肢が無いので cypress-fail-on-console-error は削除して対応することにする。
今後 Cypress が tsconfig.json の multiple extends をサポートしたらディレクトリ構造を元に戻して復活させるかもしれない(が、ディレクトリを分けた方がすっきりするのであまり気が進まない)。
状態管理アンチパターンメモ
- reducer を動的に変更しない
- reducer 関数を動的に変更するのではなく、依存している変数を state に含めて reducer は静的にすべき
先日 PRレビューしながら気づいたこと(自分が前からやっている実装パターンの理由メモ)。
React のコンポーネント実装において、 null check などの条件分岐で JSX.Element の return 文を複数に分けて書かない方が見やすい。 hooks は if 文によって呼んだり呼ばれなかったりするとエラーになるため、 early return パターンを用いることはできず、 hooks を使わないのはもっとまずいので、React コンポーネント実装において return 文は常に関数末尾にまとめられることになる。それならば、一つの return 文にまとめてしまって三項演算子 cond ? <A /> : <B />
を使って書いた方が宣言的になり、そのコンポーネントの view の全体像が分かりやすくなる。
cond
が null check であり、 <B />
の表示前に non-null が保証された値についてさらに何か計算した結果を表示したい、というケースは考えられるが、それほど複雑になる時点でコンポーネントを分けて B
の中で計算するようにして B
を React.memo
で memoize すべきであるから、これも return 文を分ける強い理由にはならない。
Object.prototype.fromEntries
は、TypeScript の標準型定義で
interface ObjectConstructor {
fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k: string]: T; };
}
と定義されているが、これを key の型を string
に潰さないようにした
interface ObjectConstructor {
fromEntries<K extends PropertyKey, V>(
entries: Iterable<readonly [K, V]>
): Record<K, V>;
}
という型に上書きして使っていたところ、以下の例で嘘の型になってしまうという問題に遭遇した。
const entries: ['x' | 'y' | 'z', number][] = [
['x', 1],
['y', 3],
] as const;
const obj = Object.fromEntries(entries); // Record<'x' | 'y' | 'z', number>
obj.x satisfies number;
obj.y satisfies number;
obj.z satisfies number;
この例において、 entries
という変数は、型注釈が ["x" | "y" | "z", number][]
となっているが実際の値として z
は含まれていない。そしてこの entries
を先ほどの fromEntries
に渡すと、ランタイムの値としては { x: 1, y: 3 }
というオブジェクトになるが、型注釈が { x: number, y: number, z: number }
となり、 z
にもアクセスできる型になってしまう。
よって、 fromEntries
の戻り値には、(少なくとも key が union 型の entries
が入ってくるケースに関しては) Partial
を付ける必要がある。
そこで、以下のように型を改善した。
interface ObjectConstructor {
fromEntries<K extends PropertyKey, V>(
entries: Iterable<readonly [K, V]>
): IsUnion<K> extends true
? Partial<Record<K, V>>
: Record<K, V>;
}
type IsNever<T> = [T] extends [never] ? true : false;
type BoolNot<A extends boolean> =
TypeEq<A, true> extends true
? false
: TypeEq<A, false> extends true
? true
: never;
type IsUnion<U> = _IsUnionImpl<U, U>;
/** @internal */
type _IsUnionImpl<U, K extends U> =
IsNever<U> extends true
? false
: K extends K
? BoolNot<TypeEq<U, K>>
: never;
未実装だが、引数がタプルの場合については key-value の組み合わせまで結果に反映した型を返せるようにしても良さそう。
const entries = [
['x', 1],
['y', 3],
] as const satisfies [['x', 1], ['y', 3]];
const obj = Object.fromEntries(entries) // satisfies { x: 1, y: 3 }; としたい
TypeScript の exactOptionalPropertyTypes
オプションは、有効化すると optional property をたらいまわしすることができなくなってしまう( Type 'undefined' is not assignable to type ...
とエラーが出てしまう)ので有効化しづらい。
Extract 型の使い方
table の column のうち一部だけ sortable であるような状況で、 type ColumnId = "name" | "description" | "updatedAt"
型の一部を SortableColumnId
型として定義したいとき、 単に type SortableColumnId = "name" | "updatedAt"
とするよりも type SortableColumnId = Extract<ColumnId, "name" | "updatedAt">
とした方が SortableColumnId
型が ColumnId
型の部分型になっていることを保証できて、将来的に ColumnId
が変更された場合などに追従しやすくなる。