Skip to content

Commit 7b24562

Browse files
committed
Audit type assertions
Type assertions can lead to runtime errors and should be avoided. Two alternatives: 1. Instead of asserting that certain resource properties are available through type assertions, check that they are actually available with null checks that will return null instead of an unexpected error. 2. Instead of asserting that array elements accessed via index are available through type assertions, use a non-null assertion which effectively does the same thing but without the need to cast the original type. This leaves just one type assertion (parentNode as HTMLElement) with added commentary on why it's necessary.
1 parent 213995c commit 7b24562

File tree

7 files changed

+40
-41
lines changed

7 files changed

+40
-41
lines changed

static-site/src/components/ListResources/IndividualResource.tsx

+15-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, {useState, useRef, useEffect, useContext} from 'react';
33
import styled from 'styled-components';
44
import { MdHistory } from "react-icons/md";
55
import { SetModalResourceContext } from './Modal';
6-
import { ResourceDisplayName, ResourceWithDisplayName, VersionedResource } from './types';
6+
import { ResourceDisplayName, Resource } from './types';
77
import { IconType } from 'react-icons';
88

99
export const LINK_COLOR = '#5097BA'
@@ -19,13 +19,15 @@ export const LINK_HOVER_COLOR = '#31586c'
1919
const [resourceFontSize, namePxPerChar, summaryPxPerChar] = [16, 10, 9];
2020
const iconWidth = 20; // not including text
2121
const gapSize = 10;
22-
export const getMaxResourceWidth = (displayResources: ResourceWithDisplayName[]) => {
22+
export const getMaxResourceWidth = (displayResources: Resource[]) => {
2323
return displayResources.reduce((w, r) => {
24+
if (!r.displayName || !r.updateCadence) return w
25+
2426
/* add the pixels for the display name */
2527
let _w = r.displayName.default.length * namePxPerChar;
2628
if (r.nVersions) {
2729
_w += gapSize + iconWidth;
28-
_w += ((r?.updateCadence?.summary?.length || 0) + 5 + String(r.nVersions).length)*summaryPxPerChar;
30+
_w += ((r.updateCadence.summary.length || 0) + 5 + String(r.nVersions).length)*summaryPxPerChar;
2931
}
3032
return _w>w ? _w : w;
3133
}, 200); // 200 (pixels) is the minimum
@@ -107,7 +109,7 @@ export function IconContainer({Icon, text, handleClick, color, hoverColor}: Icon
107109

108110

109111
interface IndividualResourceProps {
110-
resource: ResourceWithDisplayName
112+
resource: Resource
111113
isMobile: boolean
112114
}
113115

@@ -119,30 +121,35 @@ export const IndividualResource = ({resource, isMobile}: IndividualResourceProps
119121
if (!ref.current) return;
120122
/* The column CSS is great but doesn't allow us to know if an element is at
121123
the top of its column, so we resort to JS */
124+
125+
/* offsetTop is not available on ParentNode but we know that the parent node
126+
is an HTMLElement */
122127
if (ref.current.offsetTop===(ref.current.parentNode as HTMLElement).offsetTop) {
123128
setTopOfColumn(true);
124129
}
125130
}, []);
126131

132+
if (!setModalResource || !resource.displayName || !resource.updateCadence) return null
133+
127134
return (
128135
<Container ref={ref}>
129136

130137
<FlexRow>
131138

132139
<TooltipWrapper description={`Last known update on ${resource.lastUpdated}`}>
133-
<ResourceLinkWrapper onShiftClick={() => setModalResource && setModalResource(resource as VersionedResource)}>
140+
<ResourceLinkWrapper onShiftClick={() => setModalResource(resource)}>
134141
<Name displayName={resource.displayName} href={resource.url} topOfColumn={topOfColumn}/>
135142
</ResourceLinkWrapper>
136143
</TooltipWrapper>
137144

138145
{resource.versioned && !isMobile && (
139-
<TooltipWrapper description={(resource as VersionedResource).updateCadence.description +
146+
<TooltipWrapper description={resource.updateCadence.description +
140147
`<br/>Last known update on ${resource.lastUpdated}` +
141148
`<br/>${resource.nVersions} snapshots of this dataset available (click to see them)`}>
142149
<IconContainer
143150
Icon={MdHistory}
144-
text={`${(resource as VersionedResource).updateCadence.summary} (n=${resource.nVersions})`}
145-
handleClick={() => setModalResource && setModalResource(resource as VersionedResource)}
151+
text={`${resource.updateCadence.summary} (n=${resource.nVersions})`}
152+
handleClick={() => setModalResource(resource)}
146153
/>
147154
</TooltipWrapper>
148155
)}

static-site/src/components/ListResources/Modal.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import styled from 'styled-components';
44
import * as d3 from "d3";
55
import { MdClose } from "react-icons/md";
66
import { dodge } from "./dodge";
7-
import { VersionedResource } from './types';
7+
import { Resource } from './types';
88

9-
export const SetModalResourceContext = createContext<React.Dispatch<React.SetStateAction<VersionedResource | undefined>> | null>(null);
9+
export const SetModalResourceContext = createContext<React.Dispatch<React.SetStateAction<Resource | undefined>> | null>(null);
1010

1111
export const RAINBOW20 = ["#511EA8", "#4432BD", "#3F4BCA", "#4065CF", "#447ECC", "#4C91BF", "#56A0AE", "#63AC9A", "#71B486", "#81BA72", "#94BD62", "#A7BE54", "#BABC4A", "#CBB742", "#D9AE3E", "#E29E39", "#E68935", "#E56E30", "#E14F2A", "#DC2F24"];
1212
const lightGrey = 'rgba(0,0,0,0.1)';
1313

1414

1515
interface ResourceModalProps {
16-
resource?: VersionedResource
16+
resource?: Resource
1717
dismissModal: () => void
1818
}
1919

@@ -41,7 +41,7 @@ export const ResourceModal = ({resource, dismissModal}: ResourceModalProps) => {
4141
_draw(ref, resource)
4242
}, [ref, resource])
4343

44-
if (!resource) return null;
44+
if (!resource || !resource.dates || !resource.updateCadence) return null;
4545

4646
const summary = _snapshotSummary(resource.dates);
4747
return (
@@ -132,8 +132,8 @@ const Title = styled.div`
132132

133133
function _snapshotSummary(dates: string[]) {
134134
const d = [...dates].sort()
135-
const d1 = new Date(d.at( 0) as string).getTime();
136-
const d2 = new Date(d.at(-1) as string).getTime();
135+
const d1 = new Date(d.at( 0)!).getTime();
136+
const d2 = new Date(d.at(-1)!).getTime();
137137
const days = (d2 - d1)/1000/60/60/24;
138138
let duration = '';
139139
if (days < 100) duration=`${days} days`;
@@ -142,7 +142,9 @@ function _snapshotSummary(dates: string[]) {
142142
return {duration, first: d[0], last:d.at(-1)};
143143
}
144144

145-
function _draw(ref, resource: VersionedResource) {
145+
function _draw(ref, resource: Resource) {
146+
if (!resource.dates) return
147+
146148
/* Note that _page_ resizes by themselves will not result in this function
147149
rerunning, which isn't great, but for a modal I think it's perfectly
148150
acceptable */

static-site/src/components/ListResources/ResourceGroup.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MdHistory, MdFormatListBulleted, MdChevronRight } from "react-icons/md"
55
import { IndividualResource, getMaxResourceWidth, TooltipWrapper, IconContainer,
66
ResourceLinkWrapper, ResourceLink, LINK_COLOR, LINK_HOVER_COLOR } from "./IndividualResource"
77
import { SetModalResourceContext } from "./Modal";
8-
import { Group, QuickLink, Resource, ResourceWithDisplayName, VersionedResource } from './types';
8+
import { Group, QuickLink, Resource } from './types';
99

1010
interface ResourceGroupHeaderProps {
1111
group: Group
@@ -23,6 +23,8 @@ const ResourceGroupHeader = ({group, isMobile, setCollapsed, collapsible, isColl
2323
const resourcesByName = Object.fromEntries(group.resources.map((r) => [r.name, r]));
2424
const quickLinksToDisplay = (quickLinks || []).filter((ql) => !!resourcesByName[ql.name] || ql.groupName===group.groupName)
2525

26+
if (!setModalResource) return null
27+
2628
return (
2729
<HeaderContainer>
2830

@@ -77,7 +79,7 @@ const ResourceGroupHeader = ({group, isMobile, setCollapsed, collapsible, isColl
7779
)}
7880
{quickLinksToDisplay.map((ql) => (
7981
<div style={{paddingLeft: '5px'}} key={ql.name}>
80-
<ResourceLinkWrapper onShiftClick={() => {setModalResource && setModalResource(resourcesByName[ql.name] as VersionedResource)}}>
82+
<ResourceLinkWrapper onShiftClick={() => {setModalResource(resourcesByName[ql.name])}}>
8183
<ResourceLink href={`/${ql.name}`} target="_blank" rel="noreferrer">
8284
{ql.display}
8385
</ResourceLink>
@@ -127,13 +129,12 @@ export const ResourceGroup = ({group, elWidth, numGroups, sortMethod, quickLinks
127129
const [isCollapsed, setCollapsed] = useState(collapsible); // if it is collapsible, start collapsed
128130
const displayResources = isCollapsed ? group.resources.slice(0, resourcesToShowWhenCollapsed) : group.resources;
129131

130-
// after this, displayResources can/should be safely casted to ResourceWithDisplayName[]
131132
_setDisplayName(displayResources)
132133

133134
/* isMobile: boolean determines whether we expose snapshots, as we hide them on small screens */
134135
const isMobile = elWidth < 500;
135136

136-
const maxResourceWidth = getMaxResourceWidth(displayResources as ResourceWithDisplayName[]);
137+
const maxResourceWidth = getMaxResourceWidth(displayResources);
137138

138139
return (
139140
<ResourceGroupContainer>
@@ -145,7 +146,7 @@ export const ResourceGroup = ({group, elWidth, numGroups, sortMethod, quickLinks
145146

146147
<IndividualResourceContainer $maxResourceWidth={maxResourceWidth}>
147148
{/* what to do when there's only one tile in a group? */}
148-
{(displayResources as ResourceWithDisplayName[]).map((d) => (
149+
{(displayResources).map((d) => (
149150
// We use key changes to re-render the component & thus recompute the DOM position
150151
<IndividualResource resource={d} key={`${d.name}_${isCollapsed}_${elWidth}_${sortMethod}`} isMobile={isMobile}/>
151152
))}

static-site/src/components/ListResources/Showcase.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const CardOuter = styled.div`
102102

103103
const themeColors = [...theme.titleColors];
104104
const getColor = () => {
105-
themeColors.push(themeColors.shift() as string); // this will always be string; type assertion is necessary to appease tsc
105+
themeColors.push(themeColors.shift()!); // this will always be string; non-null assertion is necessary to appease tsc
106106
return themeColors.at(-1);
107107
}
108108

static-site/src/components/ListResources/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ErrorContainer } from "../../pages/404";
1313
import { TooltipWrapper } from "./IndividualResource";
1414
import {ResourceModal, SetModalResourceContext} from "./Modal";
1515
import { Showcase, useShowcaseCards} from "./Showcase";
16-
import { Card, FilterOption, Group, GroupDisplayNames, QuickLink, VersionedResource } from './types';
16+
import { Card, FilterOption, Group, GroupDisplayNames, QuickLink, Resource } from './types';
1717

1818
interface ListResourcesProps extends ListResourcesResponsiveProps {
1919
elWidth: number
@@ -45,7 +45,7 @@ function ListResources({
4545
const [resourceGroups, setResourceGroups] = useState<Group[]>([]);
4646
useSortAndFilter(sortMethod, selectedFilterOptions, groups, setResourceGroups)
4747
const availableFilterOptions = useFilterOptions(resourceGroups);
48-
const [modalResource, setModalResource ] = useState<VersionedResource>();
48+
const [modalResource, setModalResource ] = useState<Resource>();
4949

5050
if (dataFetchError) {
5151
return (

static-site/src/components/ListResources/types.ts

-11
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,6 @@ export interface Resource {
2828
updateCadence?: UpdateCadence
2929
}
3030

31-
export interface ResourceWithDisplayName extends Resource {
32-
displayName: ResourceDisplayName
33-
}
34-
35-
export interface VersionedResource extends Resource {
36-
firstUpdated: string // date
37-
dates: string[]
38-
nVersions: number
39-
updateCadence: UpdateCadence
40-
}
41-
4231
export interface ResourceDisplayName {
4332
hovered: string
4433
default: string

static-site/src/components/ListResources/useDataFetch.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function partitionByPathogen(pathVersions: PathVersions, pathPrefix: string, ver
7171
const nameParts = name.split('/');
7272
const sortedDates = [...datum[1]].sort();
7373

74-
let groupName = nameParts[0] as string;
74+
let groupName = nameParts[0]!;
7575

7676
if (!store[groupName]) store[groupName] = []
7777
const resourceDetails: Resource = {
@@ -80,18 +80,18 @@ function partitionByPathogen(pathVersions: PathVersions, pathPrefix: string, ver
8080
nameParts,
8181
sortingName: _sortableName(nameParts),
8282
url: `/${pathPrefix}${name}`,
83-
lastUpdated: sortedDates.at(-1) as string,
83+
lastUpdated: sortedDates.at(-1)!,
8484
versioned
8585
};
8686
if (versioned) {
87-
resourceDetails.firstUpdated = sortedDates[0] as string;
88-
resourceDetails.lastUpdated = sortedDates.at(-1) as string;
87+
resourceDetails.firstUpdated = sortedDates[0]!;
88+
resourceDetails.lastUpdated = sortedDates.at(-1)!;
8989
resourceDetails.dates = sortedDates;
9090
resourceDetails.nVersions = sortedDates.length;
9191
resourceDetails.updateCadence = updateCadence(sortedDates.map((date)=> new Date(date)));
9292
}
9393

94-
(store[groupName] as Resource[]).push(resourceDetails)
94+
(store[groupName])!.push(resourceDetails)
9595

9696
return store;
9797
}
@@ -105,12 +105,12 @@ function partitionByPathogen(pathVersions: PathVersions, pathPrefix: string, ver
105105
function groupsFrom(partitions: Partitions, pathPrefix: string, defaultGroupLinks: boolean, groupDisplayNames: GroupDisplayNames) {
106106

107107
return Object.keys(partitions).map(groupName => {
108-
const resources = partitions[groupName] as Resource[];
108+
const resources = partitions[groupName]!;
109109
const groupInfo: Group = {
110110
groupName: groupName,
111111
nResources: resources.length,
112112
nVersions: resources.reduce((total, r) => r.nVersions ? total+r.nVersions : total, 0) || undefined,
113-
lastUpdated: resources.map((r) => r.lastUpdated).sort().at(-1) as string,
113+
lastUpdated: resources.map((r) => r.lastUpdated).sort().at(-1)!,
114114
resources,
115115
}
116116
/* add optional properties */

0 commit comments

Comments
 (0)