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}
+
{JSON.stringify(event, null, ' ')}
+
+ ) default: + exhaustedTypeWarning('Unexpected event type', event.type) 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,