はじめに
日常の開発で「なんてことはないけれど、言われてみればそうだよな」と思った小さなテクニックを、Slackの自分宛メモしていたので、掘り起こしていく。アドベントカレンダー終わりまで、メモの山の中から他にもあれば順次更新していこうと思います。
1. 分割代入で残余プロパティを抽出する
課題
大きなオブジェクトから一部のプロパティを除外したコピーを作成したい場面に遭遇した。
よりスマートにかけないかとかと検討していたら分割代入を使えば良いと気づいた。
引き算的な使い方をあまりしたことないなと思ったので、メモしていた。
// 手動コピー:プロパティが多いと漏れやすく、保守が困難
const userDataWithoutMetadata = {
name: originalUserData.name,
email: originalUserData.email,
// ... 大量のプロパティを列挙
}
// delete使用:元オブジェクトを変更するリスク、型安全性の欠如
const userDataWithoutMetadata = { ...originalUserData }
delete userDataWithoutMetadata.id
delete userDataWithoutMetadata.password
解決策:残余プロパティの分割代入
// オリジナルのユーザーデータから特定フィールドを除外してコピーを作成
const {
id: _id,
password: _password,
createdAt: _createdAt,
updatedAt: _updatedAt,
...userDataWithoutMetadata
} = originalUserData
2. never型を使ったExhaustive Check
概要
switch文において、すべてのケースを網羅していることをコンパイル時に保証するテクニック。Union型に新しいメンバーが追加された際、対応漏れをコンパイルエラーとして検出できる。
TypeScriptのnever型は「決して発生しない値」を表す。すべてのcaseが処理された後のdefault節では、viewTypeの型は理論上「どの型にも該当しない」= neverになる。
// すべてのケースを処理した後
const _exhaustiveCheck: never = viewType // viewType は never 型になっている
Union型に新しいメンバーを追加した場合:
// 'detail' を追加
type ViewType = 'list' | 'grid' | 'card' | 'table' | 'detail'
case 'detail':を追加し忘れると、default節でviewTypeが'detail'型のままになり、never型への代入でコンパイルエラーが発生する:
Type 'string' is not assignable to type 'never'.
サンプルコード
// 表示タイプの定義
type ViewType = 'list' | 'grid' | 'card' | 'table'
const renderView = (viewType: ViewType): React.ReactNode => {
switch (viewType) {
case 'list':
return <ListView />
case 'grid':
return <GridView />
case 'card':
return <CardView />
case 'table':
return <TableView />
default: {
// exhaustive check - すべてのケースが網羅されていれば、ここには到達しない
const _exhaustiveCheck: never = viewType
return _exhaustiveCheck
}
}
}
参考
より詳しい解説があったのでこちらを参照されると良いと思います:
https://azukiazusa.dev/blog/exhaustive-checks-in-typescript/
3. useEffectには処理の意図をコメントで明示する
課題
useEffectは処理内容を読まないと意図が理解できないことが多い。依存配列と実行タイミングの関係、副作用の目的など、コードだけでは伝わりにくい情報がある。
解決策:処理の概要をコメントで明示する
// Bad: 何をしているか読まないとわからない
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser)
return () => controller.abort()
}, [userId])
// Good: 処理の意図が明確
// ユーザーIDが変更されたら、該当ユーザーの情報を取得する
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser)
return () => controller.abort()
}, [userId])
ポイント
- 何をしているかではなくなぜそうしているかを書く
- 依存配列の意図(何が変わったときに実行されるべきか)を明示する
- クリーンアップ処理がある場合はその理由も記載する
たしかに〜と思った参考元
以下メモの山から掘り起こせ次第、更新しておこうと思います!