【TypeScript】タイムゾーンを吸収するbranded type
こんにちは、フリーランスエンジニアの太田雅昭です。
タイムゾーン問題
DBによってはタイムゾーンを保存することがデフォルトではできません。例えば、PostgreSQLではタイムゾーンを一緒にフィールドに含めることができますが、MySQLなどは含めることができないため、面倒なことになります。
DBでUTCで保存してフロントではJSTで扱いたいということがあります。たとえばLaravelだと内部タイムゾーンはUTCが推奨されています。その関係から、やはりUTCで扱ってフロントではJSTとなります。
そうした場合、一般的にはフロントで解決するのが定石のようです ( ChatGPT談 )
TypeScriptで専用の型を作る
以下のようにして、専用の型を作ると、煩わしさから少しでも解放されます。Branded Typeを用いて、クライアントで扱う型を切り分けています。なお簡略化のためそのままDayjsに変換しています。
import dayjs, { Dayjs } from 'dayjs';
// types
declare const clientDayjsSymbol: unique symbol;
export type ClientDayjs = Dayjs & { readonly [clientDayjsSymbol]: true }
// utils
export function apiToClientDayjs(d: string | null | undefined): ClientDayjs | null {
if (!d) return null;
return dayjs(d)
.add(9, 'hour') as ClientDayjs;
}
export function clientToApiDatetime(d: ClientDayjs | null | undefined): string | null {
if (!d) return null;
return d
.subtract(9, 'hour')
.format('YYYY-MM-DD HH:mm:ss') as string;
}
// test
function test() {
// apiからの値。文字列
const apiValues: (string | null)[] = [
'2025-01-01',
'2025-01-01 21:00:00',
null,
]
for (const apiValue of apiValues) {
// client用に変換
const clientDayjs = apiToClientDayjs(apiValue);
// ここで色々処理する
// ...
// api用に変換
const apiDatetime = clientToApiDatetime(clientDayjs);
// ここでapiに投げる
// ...
// 結果
console.log({
apiValue,
clientDate: clientDayjs?.format('YYYY-MM-DD HH:mm:ss'),
apiDatetime
});
// {
// apiValue: '2025-01-01',
// clientDate: '2025-01-01 09:00:00',
// apiDatetime: '2025-01-01 00:00:00'
// }
// {
// apiValue: '2025-01-01 21:00:00',
// clientDate: '2025-01-02 06:00:00',
// apiDatetime: '2025-01-01 21:00:00'
// }
// {
// apiValue: null,
// clientDate: undefined,
// apiDatetime: null
// }
}
}
test();
その他の方法
mySQLはフィールドにタイムゾーンを含むことがネイティブではできませんが、INTは入れれます。そのためUNIX TIMESTAMPをつかってmsで保持するのも楽です。これなら変換作業も要りません。