Skip to content

Commit

Permalink
Add type safety for JSON parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
SBence committed Apr 21, 2024
1 parent 8e1a38f commit f31291c
Show file tree
Hide file tree
Showing 10 changed files with 56 additions and 11 deletions.
1 change: 0 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ module.exports = {
},
},
],
"@typescript-eslint/no-unsafe-assignment": "off",
"lingui/no-unlocalized-strings": [
"error",
{
Expand Down
5 changes: 4 additions & 1 deletion src/components/menu/RestoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Dispatch, SetStateAction } from "react";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import { appendSet } from "../../store/slices/countersSlice";
import { t, Trans } from "@lingui/macro";
import safeLoadJson from "../../utils/safeLoadJson";
import isCounters from "../../types/predicates/isCounters";

export default function RestoreButton({
setMenuOpened,
Expand All @@ -28,7 +30,8 @@ export default function RestoreButton({
return;
}
try {
const uploadedJson = JSON.parse(uploadedText);
const uploadedJson = safeLoadJson(uploadedText, isCounters);
if (!uploadedJson) throw new Error();
dispatch(appendSet({ counters: uploadedJson }));
notifications.show({
message: t`Successfully restored backup.`,
Expand Down
1 change: 1 addition & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export async function dynamicActivate() {
locales.includes(getLanguageCode(language)),
) ?? defaultLocale,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { messages } = await import(`./locales/${locale}.ts`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
i18n.load(locale, messages);
Expand Down
8 changes: 1 addition & 7 deletions src/store/slices/countersSlice.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { v4 as uuidv4 } from "uuid";

interface Counter {
name: string;
count: number;
}

type Counters = Record<string, Counter>;
import { Counters } from "../../types/Counters";

const DEFAULT_COUNTER = {
name: "",
Expand Down
8 changes: 6 additions & 2 deletions src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import accentColorSlice from "./slices/accentColorSlice";
import countersSlice from "./slices/countersSlice";
import safeLoadJson from "../utils/safeLoadJson";
import isSavedState from "../types/predicates/isSavedState";

const savedState = localStorage.getItem("state");
const savedStateJson = localStorage.getItem("state");

export const store = configureStore({
reducer: combineReducers({
accentColor: accentColorSlice,
counters: countersSlice,
}),
preloadedState: savedState ? JSON.parse(savedState) : undefined,
preloadedState: savedStateJson
? safeLoadJson(savedStateJson, isSavedState)
: undefined,
});

store.subscribe(() => {
Expand Down
6 changes: 6 additions & 0 deletions src/types/Counters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface Counter {
name: string;
count: number;
}

export type Counters = Record<string, Counter>;
6 changes: 6 additions & 0 deletions src/types/SavedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Counters } from "./Counters";

export interface SavedState {
accentColor: string;
counters: Counters;
}
14 changes: 14 additions & 0 deletions src/types/predicates/isCounters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Counters } from "../Counters";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default (counters: any): counters is Counters => {
for (const key in counters) {
if (Object.prototype.hasOwnProperty.call(counters, key)) {
const counter = (counters as Counters)[key];
if (!("name" in counter && "count" in counter)) return false;
} else {
return false;
}
}
return true;
};
8 changes: 8 additions & 0 deletions src/types/predicates/isSavedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SavedState } from "../SavedState";
import isCounters from "./isCounters";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default (savedState: any): savedState is SavedState => {
if (!("accentColor" in savedState && "counters" in savedState)) return false;
return isCounters((savedState as SavedState).counters);
};
10 changes: 10 additions & 0 deletions src/utils/safeLoadJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default <T>(
json: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
checkFunction: (object: any) => object is T,
) => {
if (!json) return;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const maybeSavedState = JSON.parse(json);
if (checkFunction(maybeSavedState)) return maybeSavedState;
};

0 comments on commit f31291c

Please sign in to comment.