Skip to content

Commit 1b4ce1b

Browse files
authored
ref(eslint): enable typescript-eslint/no-base-to-string (#91342)
This PR enables the [no-base-to-string](https://typescript-eslint.io/rules/no-base-to-string/) typed eslint rule. This rule errors when we try to call `toString` or `toLocaleString` on something that potentially doesn’t produce useful information when stringified. It also catches string concatenations with `+` and interpolations in template literals `${expr}`. In our code-base, this often happens when we call `String(label)`, where label is typed as `React.ReactNode`. While a ReactNode _can_ be a string, if it’s a ReactElement like `<span>foo</span>`, the stringification would just yield `[object Object]`. The fix is to either narrow the type to e.g. `string | number` (depending on how its used) if we intend to only use it with values that are stringifiable or to avoid making a string out of it. Sometimes, I had to `eslint-disable` because we’d need a larger refactor.
1 parent b8796c4 commit 1b4ce1b

File tree

52 files changed

+121
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+121
-106
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const typeAwareLintRules = {
4343
rules: {
4444
'@typescript-eslint/await-thenable': 'error',
4545
'@typescript-eslint/no-array-delete': 'error',
46+
'@typescript-eslint/no-base-to-string': 'error',
4647
'@typescript-eslint/no-for-in-array': 'error',
4748
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
4849
'@typescript-eslint/prefer-optional-chain': 'error',

static/app/components/actions/archive.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ const IGNORE_DURATIONS = [
3131

3232
const IGNORE_COUNTS = [1, 10, 100, 1000, 10000, 100000];
3333

34-
const IGNORE_WINDOWS: Array<SelectValue<number>> = [
34+
const IGNORE_WINDOWS = [
3535
{value: ONE_HOUR, label: t('per hour')},
3636
{value: ONE_HOUR * 24, label: t('per day')},
3737
{value: ONE_HOUR * 24 * 7, label: t('per week')},
38-
];
38+
] satisfies Array<SelectValue<number>>;
3939

4040
interface ArchiveActionProps {
4141
onUpdate: (params: GroupStatusResolution) => void;

static/app/components/breadcrumbs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function Breadcrumbs({crumbs, linkLastItem = false, ...props}: Props) {
110110
const {label, to, preservePageFilters, key} = crumb;
111111
const labelKey = typeof label === 'string' ? label : '';
112112
const mapKey =
113-
(key ?? typeof to === 'string') ? `${labelKey}${to}` : `${labelKey}${index}`;
113+
key ?? (typeof to === 'string' ? `${labelKey}${to}` : `${labelKey}${index}`);
114114

115115
return (
116116
<Fragment key={mapKey}>

static/app/components/charts/optionSelector.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ function OptionSelector({
5555
const mappedOptions = useMemo(() => {
5656
return options.map(opt => ({
5757
...opt,
58+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
5859
textValue: String(opt.label),
60+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
5961
label: <Truncate value={String(opt.label)} maxLength={60} expandDirection="left" />,
6062
}));
6163
}, [options]);

static/app/components/core/badge/featureBadge.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,10 @@ function InnerFeatureBadge({
5656
...props
5757
}: FeatureBadgeProps) {
5858
const indicatorColors = useFeatureBadgeIndicatorColor();
59-
const title = tooltipProps?.title?.toString() ?? defaultTitles[type] ?? '';
59+
const title = tooltipProps?.title ?? defaultTitles[type] ?? '';
6060

6161
return (
62-
<Tooltip
63-
title={title ?? defaultTitles[type]}
64-
position="right"
65-
{...tooltipProps}
66-
skipWrapper
67-
>
62+
<Tooltip title={title} position="right" {...tooltipProps} skipWrapper>
6863
{variant === 'badge' || variant === 'short' ? (
6964
<StyledBadge type={type} {...props}>
7065
{variant === 'short' ? shortLabels[type] : labels[type]}

static/app/components/core/compactSelect/utils.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,24 @@ export function getDisabledOptions<Value extends SelectKey>(
105105
export function getHiddenOptions<Value extends SelectKey>(
106106
items: Array<SelectOptionOrSectionWithKey<Value>>,
107107
search: string,
108-
limit = Infinity,
109-
filterOption?: (opt: SelectOption<Value>, search: string) => boolean
108+
limit = Infinity
110109
): Set<SelectKey> {
111110
//
112111
// First, filter options using `search` value
113112
//
114-
const _filterOption =
115-
filterOption ??
116-
((opt: SelectOption<Value>) =>
117-
`${opt.label ?? ''}${opt.textValue ?? ''}`
118-
.toLowerCase()
119-
.includes(search.toLowerCase()));
113+
const filterOption = (opt: SelectOption<Value>) =>
114+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
115+
`${opt.label ?? ''}${opt.textValue ?? ''}`
116+
.toLowerCase()
117+
.includes(search.toLowerCase());
120118

121119
const hiddenOptionsSet = new Set<SelectKey>();
122120
const remainingItems = items
123121
.flatMap<SelectOptionOrSectionWithKey<Value> | null>(item => {
124122
if ('options' in item) {
125123
const filteredOptions = item.options
126124
.map(opt => {
127-
if (_filterOption(opt, search)) {
125+
if (filterOption(opt)) {
128126
return opt;
129127
}
130128

@@ -136,7 +134,7 @@ export function getHiddenOptions<Value extends SelectKey>(
136134
return filteredOptions.length > 0 ? {...item, options: filteredOptions} : null;
137135
}
138136

139-
if (_filterOption(item, search)) {
137+
if (filterOption(item)) {
140138
return item;
141139
}
142140

static/app/components/dropdownAutoComplete/menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface MenuProps
118118
/**
119119
* Message to display when there are no items initially
120120
*/
121-
emptyMessage?: React.ReactNode;
121+
emptyMessage?: string;
122122

123123
/**
124124
* If this is undefined, autocomplete filter will use this value instead of the

static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class Candidates extends Component<Props, State> {
105105
return false;
106106
}
107107

108+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
108109
if (!defined(info) || !String(info).trim()) {
109110
return false;
110111
}

static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,9 +1851,8 @@ function ChecklistDoneNote() {
18511851
'You completed all of the steps above. Capture a new event to verify your setup!'
18521852
)}
18531853
{isSelfHosted
1854-
? ' ' +
1855-
tct(
1856-
'If the newly captured event is still not sourcemapped, please check the logs of the [symbolicator] service of your self-hosted instance.',
1854+
? tct(
1855+
' If the newly captured event is still not sourcemapped, please check the logs of the [symbolicator] service of your self-hosted instance.',
18571856
{
18581857
symbolicator: <MonoBlock>symbolicator</MonoBlock>,
18591858
}

static/app/components/externalIssues/ticketRuleModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ export default function TicketRuleModal({
360360

361361
if (!found) {
362362
errors[field.name] = (
363+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
363364
<FieldErrorLabel>{`Could not fetch saved option for ${field.label}. Please reselect.`}</FieldErrorLabel>
364365
);
365366
}

static/app/components/forms/controls/radioGroup.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,10 @@ function RadioGroup<C extends string>({
8888
<RadioLineItem index={index} aria-checked={value === id} disabled={disabled}>
8989
<Radio
9090
name={groupName}
91-
aria-label={name?.toString()}
91+
aria-label={name?.toString()} // eslint-disable-line @typescript-eslint/no-base-to-string
9292
disabled={disabled}
9393
checked={value === id}
94-
onChange={(e: React.FormEvent<HTMLInputElement>) =>
95-
!disabled && onChange(id, e)
96-
}
94+
onChange={e => !disabled && onChange(id, e)}
9795
/>
9896
<RadioLineText disabled={disabled}>{name}</RadioLineText>
9997
{description && (

static/app/components/forms/fields/segmentedRadioField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function RadioControlGroup<C extends string>({
7373
<InteractionStateLayer />
7474
<Radio
7575
name={groupName}
76-
aria-label={name?.toString()}
76+
aria-label={name?.toString()} // eslint-disable-line @typescript-eslint/no-base-to-string
7777
disabled={disabled}
7878
checked={value === id}
7979
onChange={(e: React.FormEvent<HTMLInputElement>) =>

static/app/components/group/groupSummary.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ export function GroupSummary({
189189
{
190190
id: 'resources',
191191
title: t('Resources'),
192+
193+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
192194
insight: `${isValidElement(config.resources?.description) ? '' : (config.resources?.description ?? '')}\n\n${config.resources?.links?.map(link => `[${link.text}](${link.link})`).join(' • ') ?? ''}`,
193195
insightElement: isValidElement(config.resources?.description)
194196
? config.resources?.description

static/app/components/keyValueData/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ export function Card({
163163
? truncatedItems.sort((a, b) => a.item.subject.localeCompare(b.item.subject))
164164
: truncatedItems;
165165

166-
const componentItems = orderedItems.map((itemProps, i) => (
167-
<Content expandLeft={expandLeft} key={`content-card-${title}-${i}`} {...itemProps} />
166+
const componentItems = orderedItems.map((itemProps, index) => (
167+
<Content expandLeft={expandLeft} key={String(index)} {...itemProps} />
168168
));
169169

170170
return (

static/app/components/modals/projectCreationModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PlatformPicker, {
1717
type Category,
1818
type Platform,
1919
} from 'sentry/components/platformPicker';
20+
import type {TeamOption} from 'sentry/components/teamSelector';
2021
import TeamSelector from 'sentry/components/teamSelector';
2122
import {t} from 'sentry/locale';
2223
import ProjectsStore from 'sentry/stores/projectsStore';
@@ -48,7 +49,7 @@ export default function ProjectCreationModal({
4849
undefined
4950
);
5051
const [projectName, setProjectName] = useState('');
51-
const [team, setTeam] = useState<Team | undefined>(undefined);
52+
const [team, setTeam] = useState<string | undefined>(undefined);
5253
const [creating, setCreating] = useState(false);
5354
const api = useApi();
5455
const organization = useOrganization();
@@ -198,7 +199,7 @@ export default function ProjectCreationModal({
198199
clearable={false}
199200
value={team}
200201
placeholder={t('Select a Team')}
201-
onChange={(choice: any) => setTeam(choice.value)}
202+
onChange={(choice: TeamOption) => setTeam(choice.value)}
202203
teamFilter={(tm: Team) => tm.access.includes('team:admin')}
203204
/>
204205
</div>

static/app/components/organizations/projectPageFilter/index.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,11 @@ export function ProjectPageFilter({
345345
);
346346

347347
// ProjectPageFilter will try to expand to accommodate the longest project slug
348-
const longestSlugLength = flatOptions
349-
.slice(0, 25)
350-
.reduce(
351-
(acc, cur) => (String(cur.label).length > acc ? String(cur.label).length : acc),
352-
0
353-
);
348+
const longestSlugLength = flatOptions.slice(0, 25).reduce(
349+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
350+
(acc, cur) => (String(cur.label).length > acc ? String(cur.label).length : acc),
351+
0
352+
);
354353

355354
// Calculate an appropriate width for the menu. It should be between 22 and 28em.
356355
// Within that range, the width is a function of the length of the longest slug.

static/app/components/replays/replayTagsTableRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
1818

1919
interface Props {
2020
name: string;
21-
values: ReactNode[];
22-
generateUrl?: (name: string, value: ReactNode) => LocationDescriptor;
21+
values: string[];
22+
generateUrl?: (name: string, value: string) => LocationDescriptor;
2323
}
2424

2525
const expandedViewKeys = [

static/app/components/scoreCard.tsx

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ function ScoreCard({
2828
className,
2929
renderOpenButton,
3030
isTooltipHoverable,
31-
isEstimate,
3231
}: ScoreCardProps) {
33-
const value = score ?? '\u2014';
34-
const displayScore = isEstimate && value !== '0' ? `~${value}` : value;
32+
const displayScore = score ?? '\u2014';
3533

3634
return (
3735
<ScorePanel className={className}>
@@ -52,7 +50,6 @@ function ScoreCard({
5250

5351
<ScoreWrapper>
5452
<Score>{displayScore}</Score>
55-
{isEstimate && <Asterisk>*</Asterisk>}
5653
{defined(trend) && (
5754
<Trend trendStatus={trendStatus}>
5855
<TextOverflow>{trend}</TextOverflow>
@@ -128,15 +125,4 @@ export const Trend = styled('div')<TrendProps>`
128125
overflow: hidden;
129126
`;
130127

131-
const Asterisk = styled('div')`
132-
color: grey;
133-
font-size: ${p => p.theme.fontSizeRelativeSmall};
134-
display: inline-block;
135-
width: 10pt;
136-
height: 10pt;
137-
position: relative;
138-
top: -10pt;
139-
margin-left: ${space(0.25)};
140-
`;
141-
142128
export default ScoreCard;

static/app/components/slider/index.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useCallback, useImperativeHandle, useMemo, useRef} from 'react';
1+
import {Fragment, useCallback, useImperativeHandle, useMemo, useRef} from 'react';
22
import isPropValid from '@emotion/is-prop-valid';
33
import styled from '@emotion/styled';
44
import {useNumberFormatter} from '@react-aria/i18n';
@@ -211,11 +211,15 @@ export function Slider({
211211
<SliderLabelWrapper className="label-container">
212212
<SliderLabel {...labelProps}>{label}</SliderLabel>
213213
<SliderLabelOutput {...outputProps}>
214-
{nThumbs > 1
215-
? `${getFormattedValue(selectedRange[0]!)}${getFormattedValue(
216-
selectedRange[1]!
217-
)}`
218-
: getFormattedValue(selectedRange[1]!)}
214+
{nThumbs > 1 ? (
215+
<Fragment>
216+
{getFormattedValue(selectedRange[0]!)}
217+
{'-'}
218+
{getFormattedValue(selectedRange[1]!)}
219+
</Fragment>
220+
) : (
221+
getFormattedValue(selectedRange[1]!)
222+
)}
219223
</SliderLabelOutput>
220224
</SliderLabelWrapper>
221225
)}

static/app/components/stories/jsxProperty.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export default function JSXProperty({name, value}: Props) {
3939
if (value.type === JSXNode) {
4040
return <code data-property="element">{[`${name}={`, value, '}']}</code>;
4141
}
42-
return <code data-property="element">{`${name}=${value}`}</code>;
42+
return (
43+
<code data-property="element">
44+
{`${name}=`}
45+
{value}
46+
</code>
47+
);
4348
}
4449
return <code data-property="object">{`${name}={${JSON.stringify(value)}}`}</code>;
4550
}

static/app/components/teamSelector.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ type TeamActor = {
141141
type: 'team';
142142
};
143143

144-
type TeamOption = GeneralSelectValue & {
144+
export type TeamOption = GeneralSelectValue & {
145145
actor: TeamActor | null;
146146
searchKey: string;
147147
};
@@ -413,6 +413,4 @@ const AddToProjectButton = styled(Button)`
413413
export {TeamSelector};
414414

415415
// TODO(davidenwang): this is broken due to incorrect types on react-select
416-
export default withOrganization(TeamSelector) as unknown as (
417-
p: Omit<Props, 'organization'>
418-
) => React.JSX.Element;
416+
export default withOrganization(TeamSelector);

static/app/locale.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ function parseComponentTemplate(template: string): ParsedTemplate {
244244
function renderTemplate(
245245
template: ParsedTemplate,
246246
components: ComponentMap
247-
): React.ReactNode {
247+
): React.JSX.Element {
248248
let idx = 0;
249249

250250
function renderGroup(name: string, id: string) {
@@ -304,6 +304,7 @@ function mark<T extends React.ReactNode>(node: T): T {
304304
_store: {},
305305
};
306306

307+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
307308
proxy.toString = () => '✅' + node + '✅';
308309
// TODO(TS): Should proxy be created using `React.createElement`?
309310
return proxy as any as T;
@@ -406,7 +407,7 @@ function gettextComponentTemplate(
406407
components: ComponentMap
407408
): React.JSX.Element {
408409
const parsedTemplate = parseComponentTemplate(getClient().gettext(template));
409-
return mark(renderTemplate(parsedTemplate, components) as React.JSX.Element);
410+
return mark(renderTemplate(parsedTemplate, components));
410411
}
411412

412413
/**

static/app/utils/profiling/gl/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ export function computeHighlightedBounds(
568568
return [bounds[0] - trim.length + 1, bounds[1] - trim.length + 1];
569569
}
570570

571-
throw new Error(`Unhandled case: ${JSON.stringify(bounds)} ${trim}`);
571+
throw new Error(`Unhandled case: ${JSON.stringify(bounds)} ${JSON.stringify(trim)}`);
572572
}
573573

574574
// Utility function to allow zooming into frames using a specific strategy. Supports

static/app/utils/useDismissAlert.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function isValid(timestamp: number, expirationDays: number) {
3939
function useDismissAlert({expirationDays = Number.MAX_SAFE_INTEGER, key}: Opts) {
4040
const [dismissedTimestamp, setDismissedTimestamp] = useLocalStorageState<
4141
undefined | string
42+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
4243
>(key, val => (val ? String(val) : undefined));
4344

4445
const isDismissed =

static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function UptimeAlertForm({project, handleDelete, rule}: Props) {
9595
useEffect(
9696
() =>
9797
autorun(() => {
98-
const projectSlug = formModel.getValue('projectSlug');
98+
const projectSlug = formModel.getValue<string>('projectSlug');
9999
const selectedProject = projects.find(p => p.slug === projectSlug);
100100
const apiEndpoint = rule
101101
? `/projects/${organization.slug}/${projectSlug}/uptime/${rule.id}/`

static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function YAxisSelector({
135135
return (
136136
<FieldGroup inline={false} flexibleControlStateSize error={fieldError} stacked>
137137
{aggregates.map((fieldValue, i) => (
138-
<QueryFieldWrapper key={`${fieldValue}:${i}`}>
138+
<QueryFieldWrapper key={`${i}`}>
139139
{aggregates.length > 1 && displayType === DisplayType.BIG_NUMBER && (
140140
<RadioLineItem index={i} role="radio" aria-label="aggregate-selector">
141141
<Radio

static/app/views/dashboards/widgetBuilder/components/visualize/aggregateParameterField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,5 @@ export function AggregateParameterField({
8585
/>
8686
);
8787
}
88-
throw new Error(`Unknown parameter type encountered for ${fieldValue}`);
88+
throw new Error(`Unknown parameter type encountered for ${JSON.stringify(fieldValue)}`);
8989
}

0 commit comments

Comments
 (0)