JavascriptでCookieを型安全に操作する
概要
クライアント側のJavascriptでCookieを型安全に操作する処理を考えてみました。
ユーザーが入力するフォームを想定しています。
イメージ
対象読者
- JavaScriptのフレームワークを使って開発している方
- Cookieを使っている方
いいね!してね
この記事の事例は必要に応じて今後追記していく予定です!
「新しい事例が知りたい」「他の事例も知りたい」と思った人は、ぜひこの記事にいいね👍してください。筆者のモチベーションにつながります!
それでは以下が本編です。
結論
- cookieのkey, value(object)を型で定義して、型安全にする。
- 取得した際に、オブジェクトのプロパティが全て存在するか判定する。
説明すること
- cookieの操作を型安全にする処理
cookieの操作を型安全にする処理
import type { CookieSerializeOptions } from "cookie";
import { destroyCookie, parseCookies, setCookie } from "nookies";
type ResignMemberShip = {
reasonType: string; // 退会理由
reasonDetail: string; // 詳細
agreement: boolean; // 同意する
};
export const CACHE_KEYS = {
RESIGN_MEMBERSHIP: "RESIGN_MEMBERSHIP",
} as const;
const CACHE_PROPERTY_NAMES = {
[CACHE_KEYS.RESIGN_MEMBERSHIP]: ["reasonType", "reasonDetail", "agreement"],
};
type CacheKeys = (typeof CACHE_KEYS)[keyof typeof CACHE_KEYS];
type CacheValues = {
[CACHE_KEYS.RESIGN_MEMBERSHIP]: ResignMemberShip;
};
const DEFAULT_COOKIE_OPTIONS: CookieSerializeOptions = {
httpOnly: false, // クライアントの実装なので常にfalse
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7, // 1週間
path: "/", // すべてのページでアクセス可能
};
export const setCache = <K extends CacheKeys, V extends CacheValues[K]>(
key: K,
object: V,
options?: CookieSerializeOptions,
): void => {
const cookieOptions = {
...DEFAULT_COOKIE_OPTIONS,
...options,
};
const value = JSON.stringify(object);
setCookie(null, key, value, cookieOptions);
};
export const getCache = <K extends CacheKeys, V extends CacheValues[K] | null>(
key: K,
): V | null => {
const cookies = parseCookies();
const cookieValue = cookies[key] as string | undefined;
const parseValue = (value: string): V | null => {
return JSON.parse(value);
};
try {
if (typeof cookieValue === "string" && cookieValue) {
const parsedObject = parseValue(cookieValue);
// パースした結果がnullの場合はnullを返す
if (parsedObject === null) {
return null;
}
// 期待する型のプロパティがすべて存在するかチェック
const allPropertiesExist = Object.keys(parsedObject).every((k) => {
return CACHE_PROPERTY_NAMES[key].includes(k);
});
// すべてのプロパティが存在する場合のみ返す
if (parsedObject && allPropertiesExist) {
return parsedObject;
}
}
return null;
} catch (error) {
console.error(error); // エラー処理は一旦省略
return null;
}
};
export const removeCache = (
key: CacheKeys,
options?: CookieSerializeOptions,
): void => {
destroyCookie(null, key, options);
};
型についての説明
keyにはユースケース名を指定するようにしました
ユースケースに対応するキャッシュの値をオブジェクト単位で保存します。
export const CACHE_KEYS = {
RESIGN_MEMBERSHIP: "RESIGN_MEMBERSHIP",
} as const;
type ResignMemberShip = {
reasonType: string;
reasonDetail: string;
agreement: boolean;
};
type CacheValues = {
[CACHE_KEYS.RESIGN_MEMBERSHIP]: ResignMemberShip;
};
さらに、keyを指定するとvalueの型を要求する ようにしました。
<K extends CacheKeys, V extends CacheValues[K]>
これによって、keyを指定した段階で値の型が決まって、型チェックが実行されるようになります。
setCacheの例
getCacheの例
なんでオブジェクトをJSON文字列で操作するの?
オブジェクトをJSON文字列として操作する理由は、パース時に保存時の型(Number, boolean, Array)で復元したいからです。(そのまま出し入れすると文字列になってしまう)
なので「ユースケースごとにオブジェクト単位で保存する」のが良さそうと思いました!
値の検証
cookieは、有効期限があり揮発性のあるキャッシュです。ユーザーがブラウザ操作して変更/削除することもできます。そのためGet処理では下記の全てを満たす場合にのみ、値(と期待する型)を返すようにしました。
- keyに対応するcookieの値(JSON文字列)を取得できるか?
- 1の値をJSON.parseできるか?
- 2の値が存在するか?
- 3の値に期待する型のプロパティが全て存在するか?
const cookies = parseCookies();
const cookieValue = cookies[key] as string | undefined;
const parseValue = (value: string): V | null => {
try {
return JSON.parse(value) as V;
} catch {
return null;
}
};
try {
// 1. keyに対応するcookieの値(JSON文字列)を取得できるか?
if (typeof cookieValue === "string" && cookieValue) {
// 2. 1の値をJSON.parseできるか?
const parsedObject = parseValue(cookieValue);
// 3. 2の値が存在するか?
if (parsedObject === null) {
return null;
}
// 4. 3の値に期待する型のプロパティが全て存在するか?
const allPropertiesExist = Object.keys(parsedObject).every((k) => {
return CACHE_PROPERTY_NAMES[key].includes(k);
});
// すべてのプロパティが存在する場合のみ返す
if (parsedObject && allPropertiesExist) {
return parsedObject;
}
}
return null;
} catch (error) {
console.error(error); // エラー処理は一旦省略
return null;
}
まとめ
「キー名をユースケース名にして、対応する値を型で制御する」でcookieを型安全に操作する例を説明しました。
このパターンは、他の処理でも応用が効くため覚えておくと便利かもしれません!
この記事の事例は必要に応じて今後追記していく予定です!
「新しい事例が知りたい」「他の事例も知りたい」と思った人は、ぜひこの記事にいいね👍してください。筆者のモチベーションにつながります!
Discussion
React.jsの場合の注意点
画面遷移: a 入力画面 -> b 入力確認画面 -> c 完了画面 で
a 入力画面: set, get
b 入力確認画面: get, remove
c 完了画面: -
する場合、
bでremove後にページ遷移すると、ページ遷移直前に再実行されるためuseMemoしておく必要がある。
TODO: オブジェクトの型定義
as const satisfies Hoge みたいにした方が型推論できそう(楽そう)