皆様こんにちは。
S+BIMチームのコモドドラゴンです。
最近、エリッククラプトンのライブに参戦したのですが、まさに、Wonderful tonight でした!
そんな私が今回ブログ記事として、執筆させていただくテーマは、TypeScript Tipsです。
昨今のフロントエンド界隈はSPAやSSRなどの技術が発達したのと同時に、
とても複雑化してきており、生のJSではランタイム時にしかエラーが分からなかったり、
JSDocでパラメータや戻り値の説明を補完していたりするケースも少なくないかと思います。(フロントエンドに限らず、js/tsはバックエンドで使用されることもあります)
そんな時に役に立つのが、JSのスーパーセット(上位互換)であるTypeScriptです。
引用:https://typescriptbook.jp/top/typescript-as-superset-of-javascript.svg
実際にコードをみていった方が早いと思うので弊社のS+BIMプロダクトでも使っているテクニックを紹介していきます。(※実際に提供中のプロダクトで使用しているコードとは異なります)
満足できなければ、satisfies を使ってみよう
satisfies
演算子、これはTypeScript 4.9から導入された機能です。ある式が何らかの型と一致することを確認したい一方で、推論の目的でその式の最も具体的な型を保持したいというジレンマのために開発された演算子
(https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html)
この演算子は、オブジェクトのキーや型の整合性をチェックするのに非常に便利です。
まず、基本的なところとして型注釈(Type Annotation)とsatisfiesの違いから見ていきます。
type Config = { theme: 'light' | 'dark' size: number } // 型注釈:より緩い型になる const annotationConfig: Config = { theme: 'dark', size: 16 } annotationConfig.theme // 'light' | 'dark' (緩い) // satisfies:厳密な型を保持 const satisfiesConfig = { theme: 'dark', size: 16 } satisfies Config satisfiesConfig.theme // 'dark' (厳密)
どちらも、型推論は行われますが、推論のタイミングと型の厳密さに違いがあります。
型注釈の場合
1.右辺の型推論
{ theme: 'dark', // 'dark' リテラル型として推論 size: 16 // 16 リテラル型として推論 } // → { theme: 'dark', size: 16 } 型
2.適合性チェック
{ theme: 'dark', size: 16 } が Config に代入可能? // 'dark' は 'light' | 'dark' に含まれる ✓ // 16 は number に含まれる ✓ // → 適合OK
3.最終型決定
// 注釈で指定した型で「上書き」 annotationConfig: Config = { theme: 'light' | 'dark', size: number }
satisfiesの場合
- 1、2 は同じですが、3の最終型決定が異なります。
3.最終型決定
// 推論された型をそのまま保持 satisfiesConfig: { theme: 'dark', size: 16 }
このように、型注釈は、型を明示的に上書きするのに対し、
satisfiesは、型推論結果を保ったまま(型注釈のように完全に上書きをせずに)、型付けをします。
特に、「型推論結果を保ったまま」というのがミソで、この性質を利用して、オブジェクトのキーや型の整合性をチェックすることができます。
言葉で書くとわかりにくいので、実際プロダクトで使った例を紹介します。 特に役立った場面は翻訳対応の時です。
- 例えば、以下のような日本語の翻訳ファイル(ja.ts)があるとします。
export default { management: '管理', selectAll: '全て選択', deselectAll: '全て解除', update: '更新', site: '現場', folderName: 'フォルダ名', drawingName: '図面名', issuer: '発行者', dateOfIssue: '発行日', expirationDate: '有効期限', remarks: '備考', };
- 日本語の翻訳ファイルを英語に翻訳(en.ts)したい際に漏れがないように、satisfiesを使用することができます。
import type ja from './ja'; export default { management: 'Management', selectAll: 'Select All', deselectAll: 'Cancel All', update: 'Update', site: 'Site', folderName: 'Folder Name', drawingName: 'Drawing Name', issuer: 'Issuer', dateOfIssue: 'Date of Issue', expirationDate: 'Expiration Date', remarks: 'Remarks', } satisfies typeof ja;
- 試しに、英訳ファイルから、`site` の翻訳を消してみると、エディタがしっかり教えてくれます!
Nice 静的解析ですね!!
ざっくり使い分けの方針としては、以下のようになります。 ※ただし、プロジェクトやチームによって異なる可能性があります。
型注釈を使う場面
- 変数の型を明示的に指定したい
- より抽象的な型として扱いたい
- 関数の引数・戻り値など、型を固定したい
satisfiesを使う場面
- 型の制約はチェックしつつ、具体的な型情報を保持したい
- オブジェクトのキーの整合性をチェックしたい
- 型安全性と型推論の恩恵を両方欲しい
as const でリテラル型安全ドライバー
as const(const assertion)は、変数宣言時に使用する演算子で、その値を再起的にreadonlyにした上でリテラル型として扱います。
実際のプロダクトで、CSVデータ列の解析処理で使った例を紹介します。
一週前の記事の、hono/zod-openapiで実現するAPI開発実践 OpenAPIルートの定義でもas const
を使った例がありますので、是非アツアツなHonoの世界を覗いてみてください!
for (const row of csvData) { const id = row[0]; // ID列 const user = row[1]; // ユーザ列 const timestamp = row[2]; // タイムスタンプ列 // ... }
- そこで、
as const
を使って、列の値をリテラル型として扱うことで、型安全にすることができます。
const DataColumns = { ID: 0, USER: 1, TIMESTAMP: 2, } as const;
- また、readonlyなプロパティであるため、誤って代入してしまうことも防げます。
このように、変数の再代入を防ぎつつ、不変な値をリテラル型として扱うことができるのが、as const
の利点であり、
コメントを書かずとも、型の表現力によって、コードの意図を明確にすることができます。
味のしなくなったガムは噛みたくない。型を絞り込んで、新しい味を見つけよう
型の絞り込みは、TypeScriptの強力な機能で、特定の型に対してのみ処理を行うことができます。
まずは、タグ付きユニオン(Discriminating Unions)型とNever型(網羅性チェック)を使った例を紹介します。
- 例えば、以下のようなユーザーデータを扱う場合を考えます。このデータは、タグ付きユニオン型で表現されており、
type
プロパティによって内部ユーザーと外部ユーザーを区別しています。
export type User = | { type: 'internal'; id: number; group: string } | { type: 'external'; id: string; group: string };
user.type
をswitch文の条件に取ることで型の絞り込み +never
による網羅性チェックを行うことができます
function getUserInfoSample(user: User): string { switch (user.type) { case 'internal': return `Internal User ID (number): ${user.id}, Group: ${user.group}`; case 'external': return `External User ID (string): ${user.id}, Group: ${user.group}`; default: throw new Error(user satisfies never); // 網羅性チェック } }
それぞれ詳しくみていきましょう。
タグ付きユニオン型: オブジェクトの型を区別するために、ここでは、
type
という共通のプロパティ(タグ
)を使用します。型の絞り込み: switch文の条件に
user.type
を使用することで、TypeScriptはuser
の型を自動的に絞り込みます。これにより、各ケースでuser
の型が明確になり、適切なプロパティにアクセスできます。internal の場合は、
id
がnumber
型external の場合は
id
がstring
型
Never型: 到達不可能な値を表します。
user satisfies never
は、user
がnever
型であることを確認するために使用されます。網羅性チェックとして、switch文のdefault
ケースで使用されます。- 例えば、途中のcase文を削除してみると、「externalがないよ!」ってエディタが教えてくれます。
JavaScriptでもお馴染みのtypeof/instanceof を使った型の絞り込み
TypeScriptでtypeof/instanceof を使用することで、変数の型の絞り込みが可能になります
- 以下のような条件分岐をさせる処理があったとします(ブログ用のため条件分岐が多いです 🙇)
class DatabaseError extends Error { constructor(message: string) { super(message); this.name = 'DatabaseError'; } } class NetworkError extends Error { constructor(message: string) { super(message); this.name = 'NetworkError'; } } function processData(data: string | number | boolean | DatabaseError | NetworkError) { if (typeof data === 'string') { console.log(`string data: ${data}`); } else if (typeof data === 'number') { console.log(`number data: ${data}`); } else if (typeof data === 'boolean') { console.log(`boolean data: ${data}`); } else if (data instanceof DatabaseError) { console.log(`database error: ${data.message}`); } else if (data instanceof NetworkError) { console.log(`network error: ${data.message}`); } else { return data satisfies never; } }
注目ポイントは、
typeof/instanceof
演算子を使って、data
の型を絞り込めているところです。はじめのif文
- 次のelse if文
- instanceofも同様に
これまでの例を見ていくと、TypeScriptは開発者がコードを読むときと同じように、制御フローや演算子を理解して、型を絞り込む(Narrowing
)ことができます。
いわゆる、型ガードと呼ばれるこの手法は、特定の型に対してのみ処理を行う場合や、複数の型が混在する場合に非常に有用です。
まとめ
いかがでしたでしょうか??
S+BIMチームでは、日々このようなTipsをモブプロ・ペアプロで共有しながら開発時に試行錯誤を繰り返しています。 他にもTypeScriptには便利な機能がたくさんありますが、今回は実際にプロダクトで使ったテクニックを例として紹介してみました!
さいごに
スパイダープラスでは仲間を募集中です。
SaaSを支える技術に興味がある方、建設DXのパイオニアになりたい方など大歓迎です。
ぜひ、お気軽にご連絡ください。