Skip to content

Commit 386b78c

Browse files
authored
refactor: Improve type safe of chrome storage (#182)
* refactor: Improve type safe of chrome storage * Refactor
1 parent 4b2ee5d commit 386b78c

File tree

9 files changed

+56
-33
lines changed

9 files changed

+56
-33
lines changed

src/Bookmarks/model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export type BookmarkFolderID = string
22

3+
export const isBookmarkFolderIDArray = (value: unknown): value is readonly BookmarkFolderID[] =>
4+
Array.isArray(value) && value.every((item) => typeof item === 'string')
5+
36
export type BookmarkFolder = {
47
readonly id: BookmarkFolderID
58
readonly depth: number

src/Bookmarks/repository.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Bookmark, BookmarkFolder, BookmarkFolderID, FolderCollapse, Position } from './model'
1+
import { Bookmark, BookmarkFolder, BookmarkFolderID, FolderCollapse, Position, isBookmarkFolderIDArray } from './model'
22
import { useEffect, useState } from 'react'
33
import { useChromeStorage } from '../infrastructure/chromeStorage'
44

@@ -82,11 +82,7 @@ export const useFolderCollapse = (): readonly [FolderCollapse, (newSet: FolderCo
8282
areaName: 'sync',
8383
key: 'v3.collapsedBookmarkFolderIDs',
8484
initialValue: [],
85-
assertType: (value: unknown) => {
86-
if (!Array.isArray(value)) {
87-
throw new Error('value is not array')
88-
}
89-
},
85+
isType: isBookmarkFolderIDArray,
9086
})
9187
return [new FolderCollapse(ids), (newSet: FolderCollapse) => setIDs(newSet.serialize())]
9288
}

src/ShortcutKey/repository.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ export const useShortcutMap = (): readonly [ShortcutMap, (newMap: ShortcutMap) =
66
areaName: 'sync',
77
key: 'v3.shortcutKeyMap',
88
initialValue: [],
9-
assertType: (value: unknown) => {
10-
if (!Array.isArray(value)) {
11-
throw new Error('value is not array')
12-
}
13-
},
9+
isType: (value): value is [string, string][] => Array.isArray(value),
1410
})
1511
return [new ShortcutMap(entries), (newMap: ShortcutMap) => setEntries(newMap.serialize())]
1612
}

src/Themes/model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export const allColorSchemes = ['auto', 'light', 'dark'] as const
22
export type ColorScheme = typeof allColorSchemes[number]
3+
export const isColorScheme = (value: unknown): value is ColorScheme =>
4+
allColorSchemes.some((colorScheme) => value === colorScheme)
35

46
export const allThemes = ['standard', 'solarized'] as const
57
export type Theme = typeof allThemes[number]
8+
export const isTheme = (value: unknown): value is Theme => allThemes.some((theme) => value === theme)

src/Themes/repository.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
import { ColorScheme, Theme } from './model'
1+
import { ColorScheme, Theme, isColorScheme, isTheme } from './model'
22
import { useChromeStorage } from '../infrastructure/chromeStorage'
33

44
export const useSelectedTheme = (initialValue: Theme) =>
55
useChromeStorage<Theme>({
66
areaName: 'sync',
77
key: 'v3.selectedTheme',
88
initialValue,
9-
assertType: (value: unknown) => {
10-
if (typeof value !== 'string') {
11-
throw new Error('value is not string')
12-
}
13-
},
9+
isType: isTheme,
1410
})
1511

1612
export const useSelectedColorScheme = (initialValue: ColorScheme) =>
1713
useChromeStorage<ColorScheme>({
1814
areaName: 'sync',
1915
key: 'v3.selectedColorScheme',
2016
initialValue,
21-
assertType: (value: unknown) => {
22-
if (typeof value !== 'string') {
23-
throw new Error('value is not string')
24-
}
25-
},
17+
isType: isColorScheme,
2618
})

src/Toggles/model.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defaultToggles, isToggles } from './model'
2+
3+
describe('isToggles', () => {
4+
test('non-object is not Toggles', () => {
5+
expect(isToggles(1)).toBe(false)
6+
})
7+
test('empty object is not Toggles', () => {
8+
expect(isToggles({})).toBe(false)
9+
})
10+
test('defaultToggles is Toggles', () => {
11+
expect(isToggles(defaultToggles)).toBe(true)
12+
})
13+
})

src/Toggles/model.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1+
type PropertyValidator<T> = Record<keyof T, (prop: unknown) => boolean>
2+
3+
const validateObject = <T extends object>(o: unknown, validator: PropertyValidator<T>): o is T =>
4+
typeof o === 'object' &&
5+
o !== null &&
6+
// object has all keys of T
7+
Object.keys(validator).every((key) => Object.hasOwn(o, key)) &&
8+
// all properties of object are valid
9+
Object.entries(o).every(([key, prop]) => key in validator && validator[key as keyof T](prop))
10+
111
export type Toggles = {
212
readonly topSites: boolean
313
readonly bookmarks: boolean
414
readonly indent: boolean
515
}
616

17+
const togglesValidator: PropertyValidator<Toggles> = {
18+
topSites: (prop) => typeof prop === 'boolean',
19+
bookmarks: (prop) => typeof prop === 'boolean',
20+
indent: (prop) => typeof prop === 'boolean',
21+
}
22+
23+
export const isToggles = (o: unknown): o is Toggles => validateObject(o, togglesValidator)
24+
725
export const defaultToggles: Toggles = {
826
bookmarks: true,
927
topSites: true,

src/Toggles/repository.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { Toggles, defaultToggles } from './model'
1+
import { Toggles, defaultToggles, isToggles } from './model'
22
import { useChromeStorage } from '../infrastructure/chromeStorage'
33

44
export const useToggles = () =>
55
useChromeStorage<Toggles>({
66
areaName: 'sync',
77
key: 'v3.toggles',
88
initialValue: defaultToggles,
9-
assertType: (value: unknown) => {
10-
if (typeof value !== 'object' || value === null) {
11-
throw new Error('value is not object')
12-
}
13-
},
9+
isType: isToggles,
1410
})

src/infrastructure/chromeStorage.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ type Spec<T> = {
44
areaName: chrome.storage.AreaName
55
key: string
66
initialValue: T
7-
assertType: (value: unknown) => asserts value is T
7+
isType: (value: unknown) => value is T
88
}
99

1010
export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, (newValue: T) => void] => {
@@ -39,8 +39,11 @@ const initialLoad = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) =>
3939
setStoredValue(spec.initialValue)
4040
return
4141
}
42-
spec.assertType(value)
43-
setStoredValue(value)
42+
if (spec.isType(value)) {
43+
setStoredValue(value)
44+
return
45+
}
46+
console.warn(`unknown type of storage.${spec.areaName}.${spec.key}`, value)
4447
})
4548
.catch((e) => console.error(e))
4649
}
@@ -56,8 +59,11 @@ const subscribeChange = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void
5659
setStoredValue(spec.initialValue)
5760
return
5861
}
59-
spec.assertType(newValue)
60-
setStoredValue(newValue)
62+
if (spec.isType(newValue)) {
63+
setStoredValue(newValue)
64+
return
65+
}
66+
console.warn(`unknown type of storage.${spec.areaName}.${spec.key}`, newValue)
6167
}
6268
area.onChanged.addListener(listener)
6369
return () => area.onChanged.removeListener(listener)

0 commit comments

Comments
 (0)