Open9

環境構築でハマったこと・実装アンチパターン等のメモ

Hideaki NoshiroHideaki Noshiro

後で何のためにやった設定か分からなくなることがある(特に「あえてこうしていない」系のもの)ので作業履歴として諸々残していくことにした。

Hideaki NoshiroHideaki Noshiro

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

Hideaki NoshiroHideaki Noshiro

workspace のルートの型チェック用 tsconfig.json に cypress ディレクトリを含めたら jest の型定義と cypress の型定義が conflict することが分かったため、 cypress 用の type-check 用 tsconfig と eslint.config.js は分けるようにした。

Hideaki NoshiroHideaki Noshiro

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 をサポートしたらディレクトリ構造を元に戻して復活させるかもしれない(が、ディレクトリを分けた方がすっきりするのであまり気が進まない)。

Hideaki NoshiroHideaki Noshiro

状態管理アンチパターンメモ

  • reducer を動的に変更しない
    • reducer 関数を動的に変更するのではなく、依存している変数を state に含めて reducer は静的にすべき
Hideaki NoshiroHideaki Noshiro

先日 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 の中で計算するようにして BReact.memo で memoize すべきであるから、これも return 文を分ける強い理由にはならない。

Hideaki NoshiroHideaki Noshiro

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 }; としたい
Hideaki NoshiroHideaki Noshiro

TypeScript の exactOptionalPropertyTypes オプションは、有効化すると optional property をたらいまわしすることができなくなってしまう( Type 'undefined' is not assignable to type ... とエラーが出てしまう)ので有効化しづらい。

Hideaki NoshiroHideaki Noshiro

Extract 型の使い方

table の column のうち一部だけ sortable であるような状況で、 type ColumnId = "name" | "description" | "updatedAt" 型の一部を SortableColumnId 型として定義したいとき、 単に type SortableColumnId = "name" | "updatedAt" とするよりも type SortableColumnId = Extract<ColumnId, "name" | "updatedAt"> とした方が SortableColumnId 型が ColumnId 型の部分型になっていることを保証できて、将来的に ColumnId が変更された場合などに追従しやすくなる。