diff --git a/.changelog/477.internal.md b/.changelog/477.internal.md
new file mode 100644
index 000000000..055d2c99a
--- /dev/null
+++ b/.changelog/477.internal.md
@@ -0,0 +1 @@
+Add strict type-checks that most switch statements cover all cases
diff --git a/src/app/components/Transactions/LogEvent.tsx b/src/app/components/Transactions/LogEvent.tsx
index e5961e9c7..66eb26f53 100644
--- a/src/app/components/Transactions/LogEvent.tsx
+++ b/src/app/components/Transactions/LogEvent.tsx
@@ -15,6 +15,7 @@ import { TransactionLink } from './TransactionLink'
import { SearchScope } from '../../../types/searchScope'
import { AddressSwitchOption } from '../AddressSwitch'
import { getOasisAddress } from '../../utils/helpers'
+import { exhaustedTypeWarning } from '../../../types/errors'
const EvmEventParamData: FC<{
scope: SearchScope
@@ -155,7 +156,14 @@ const DecodedLogEvent: FC<{
case RuntimeEventType.accountstransfer:
case RuntimeEventType.consensus_accountsdeposit:
case RuntimeEventType.consensus_accountswithdraw:
+ return (
+
{eventName}
diff --git a/src/app/pages/DashboardPage/LearningMaterials.tsx b/src/app/pages/DashboardPage/LearningMaterials.tsx
index 21c158d29..72507d31f 100644
--- a/src/app/pages/DashboardPage/LearningMaterials.tsx
+++ b/src/app/pages/DashboardPage/LearningMaterials.tsx
@@ -12,7 +12,7 @@ import Typography from '@mui/material/Typography'
import { styled } from '@mui/material/styles'
import { COLORS } from '../../../styles/theme/colors'
import { docs } from '../../utils/externalLinks'
-import { AppError, AppErrors } from '../../../types/errors'
+import { AppError, AppErrors, exhaustedTypeWarning } from '../../../types/errors'
import { Layer } from '../../../oasis-indexer/api'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
@@ -49,7 +49,12 @@ const getContent = (t: TFunction, layer: Layer) => {
link: docs.sapphire,
title: t('common.sapphire'),
}
+ case Layer.cipher:
+ throw new AppError(AppErrors.UnsupportedLayer)
+ case Layer.consensus:
+ throw new AppError(AppErrors.UnsupportedLayer)
default:
+ exhaustedTypeWarning('Unexpected layer', layer)
throw new AppError(AppErrors.UnsupportedLayer)
}
}
diff --git a/src/app/pages/HomePage/Graph/Graph/graph-utils.ts b/src/app/pages/HomePage/Graph/Graph/graph-utils.ts
index 236296143..b178eeb7c 100644
--- a/src/app/pages/HomePage/Graph/Graph/graph-utils.ts
+++ b/src/app/pages/HomePage/Graph/Graph/graph-utils.ts
@@ -1,5 +1,6 @@
import { ScaleToOptions } from 'react-quick-pinch-zoom'
import { Layer } from '../../../../../oasis-indexer/api'
+import { exhaustedTypeWarning } from '../../../../../types/errors'
export abstract class GraphUtils {
static getScaleTo(layer: Layer, { width, height }: { width?: number; height?: number }): ScaleToOptions {
@@ -33,7 +34,9 @@ export abstract class GraphUtils {
y: height,
}
case Layer.consensus:
+ return initialValue
default:
+ exhaustedTypeWarning('Unexpected layer', layer)
return initialValue
}
}
diff --git a/src/app/pages/HomePage/Graph/para-time-selector-utils.ts b/src/app/pages/HomePage/Graph/para-time-selector-utils.ts
index 1f5d75d1c..7abbaaba0 100644
--- a/src/app/pages/HomePage/Graph/para-time-selector-utils.ts
+++ b/src/app/pages/HomePage/Graph/para-time-selector-utils.ts
@@ -1,5 +1,6 @@
import { ParaTimeSelectorStep } from './types'
import { Layer } from '../../../../oasis-indexer/api'
+import { exhaustedTypeWarning } from '../../../../types/errors'
export abstract class ParaTimeSelectorUtils {
static getIsGraphTransparent(step: ParaTimeSelectorStep) {
@@ -28,7 +29,11 @@ export abstract class ParaTimeSelectorUtils {
case Layer.emerald:
case Layer.cipher:
return true
+ case Layer.consensus:
+ case undefined:
+ return false
default:
+ exhaustedTypeWarning('Unexpected layer', layer)
return false
}
}
diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
index 05a315e04..63524c8ef 100644
--- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
+++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
@@ -4,6 +4,7 @@ import { SearchResults } from './hooks'
import { RouteUtils } from '../../utils/route-utils'
import { isItemInScope, SearchScope } from '../../../types/searchScope'
import { Network } from '../../../types/network'
+import { exhaustedTypeWarning } from '../../../types/errors'
/** If search only finds one result then redirect to it */
export function useRedirectIfSingleResult(scope: SearchScope | undefined, results: SearchResults) {
@@ -30,11 +31,7 @@ export function useRedirectIfSingleResult(scope: SearchScope | undefined, result
redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address)
break
default:
- // The conversion of any is necessary here, since we have covered all possible subtype,
- // and TS is concluding that the only possible remaining type is "never".
- // However, if we all more result types in the future and forget to add the appropriate case here,
- // we might hit this, hence the warning.
- console.log(`Don't know how to redirect to unknown search result type ${(item as any).resultType}`)
+ exhaustedTypeWarning('Unexpected result type', item)
}
}
diff --git a/src/coin-gecko/api.ts b/src/coin-gecko/api.ts
index 76707c03a..7e160aad6 100644
--- a/src/coin-gecko/api.ts
+++ b/src/coin-gecko/api.ts
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'
import { getTickerForNetwork, NativeTicker, Ticker } from '../types/ticker'
import { Network } from '../types/network'
import { RouteUtils } from '../app/utils/route-utils'
+import { exhaustedTypeWarning } from '../types/errors'
type GetRosePriceParams = {
ids: string
@@ -68,7 +69,7 @@ export const useTokenPrice = (ticker: NativeTicker): TokenPriceInfo => {
isFree: true,
}
default:
- console.warn('Checking price of unknown token', ticker)
+ exhaustedTypeWarning('Checking price of unknown token', ticker)
return {
isLoading: false,
hasUsedCoinGecko: false,
diff --git a/src/types/errors.ts b/src/types/errors.ts
index b3c36edc4..63403efba 100644
--- a/src/types/errors.ts
+++ b/src/types/errors.ts
@@ -25,3 +25,20 @@ export interface ErrorPayload {
code: AppErrors
message: string
}
+
+// Adds strict type-check that a type was exhausted
+// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
+// https://stackoverflow.com/questions/41102060/typescript-extending-error-class
+export async function exhaustedTypeWarning(
+ messagePrefix: string,
+ exhaustedType: 'Expected type to be exhausted, but this type was not handled',
+) {
+ const message = `${messagePrefix}: Expected type to be exhausted, but this type was not handled: ${JSON.stringify(
+ exhaustedType,
+ )}`
+ if (process.env.NODE_ENV === 'production') {
+ console.warn(message)
+ } else {
+ throw new Error(message)
+ }
+}
diff --git a/src/types/ticker.ts b/src/types/ticker.ts
index 9a6c50790..5c7a19b73 100644
--- a/src/types/ticker.ts
+++ b/src/types/ticker.ts
@@ -6,7 +6,7 @@ export type NativeTicker = (typeof Ticker)[keyof typeof Ticker]
export const Ticker = {
ROSE: 'ROSE',
TEST: 'TEST',
-}
+} as const
const networkTicker: Record
= {
[Network.mainnet]: Ticker.ROSE,