TypeScript の mysql/mysql2 で RowDataPacket に悩まない
Node.js 向けの DB ドライバ mysql/mysql2 パッケージはクエリの結果を RowDataPacket
オブジェクトを配列にして返却する。このおかげで、Node.js + TypeScript を学びたての頃はどのようにして型付きの結果を得るのかわからなかったので、昔の自分のために答えだけを記載しておく。
インターフェースを使う
RowDataPacket 型の宣言は下記のようになっている。
declare interface RowDataPacket {
constructor: {
name: "RowDataPacket";
};
[column: string]: any;
[column: number]: any;
}
このままだとそのまま any
を持つ型定義がデータベースドライバ利用側に漏れてしまうので、RowDataPacket
を拡張するインターフェースを作る。
export interface IBook extends RowDataPacket {
id: string;
title: string;
price: number;
author: Author;
}
export const fetchBooks = async (): Promise<IBook[]> => {
const con = await database.getConnection();
try {
const [results] = await con.query<IBook[]>(
`
SELECT
id, title, price, author
FROM
books_table;
`
);
return results;
} catch (e) {
await con.rollback();
throw e;
} finally {
await con.end();
}
};
こうすると con.query()
で返される戻り値Prosmise<[T, FieldPacket[]>
のうち、 T
の部分は実行した SQL と定義した型が一致した状態でオブジェクトが返却される。
*ただし定義した型のプロパティとクエリ内のカラムの指定に差異がある場合は undefined になるので注意。
中身の話
TypeScript に強いわけではないので自分の解釈で記載します。間違っていたらマサカリを投げてください。
mysql/mysql2 パッケージからは我々がどんなテーブル、スキーマをデータベースに定義しているのか知らない。
そこで、query
などの関数は戻り値にどんなテーブルスキーマにも対応できるようにジェネリクス型の T
にしておき、query
関数実行時の型引数によって RowDataPacket
, OkPacket
, ResultSetHeader
などのオブジェクトを拡張したインターフェースを待ち受ける。
今回のように SELECT 文の実行結果を返すような場合は RowDataPacket
型がそれに対応するので、それを拡張したインターフェースであれば適切な型として動作する。
調べてみて
何も考えずに使い方だけ記載するだけならインターフェースの記述だけで十分だったが、ついでにコードも読んでみると単純な型定義の話だけでなく、ジェネリクスの概念も入ってくるため、TypeScript のステップアップには良さそうな題材だなと感じた。
ぶっちゃけクエリ一つにインターフェース1個生やすのもな、という気持ちがあって、インターフェースの拡張に頼らない記法があれば教えてほしいと思いました。
/ 以上
Discussion