Skip to content

[ui] Improve typechecking on qs usage #29083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025
Merged

Conversation

hellendag
Copy link
Member

@hellendag hellendag commented Apr 7, 2025

Summary & Motivation

Following the recent JS error involving parsed querystrings crashing the page, try to lock down types on our qs usage in useQueryPersistedState and similar hooks that make use of encode/decode behavior.

This PR makes use of the qs.ParsedQs type, which is the type returned by qs.parse itself. This replaces our current {[key: string]: any} type, which at present leaves us with no real recourse for typechecking our parsed values.

Now, encode/decode functions will have to be much more careful about the types involved. For instance, in the case of the error hotfixed by #28791, the decode function would now have to refine the value for qs['open-nodes'] in order to satisfy the Set<string> type in its return value. If qs['open-nodes'] were an object (as in the error case), the new Set(...) would fail typechecking, and the developer would be forced to provide it an array of strings extracted from the qs argument.

How I Tested These Changes

TS, lint, jest.

Copy link
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link

github-actions bot commented Apr 7, 2025

Deploy preview for dagit-storybook ready!

✅ Preview
https://dagit-storybook-ig1gcvn0o-elementl.vercel.app
https://dish-fe-842-try-to-improve.components-storybook.dagster-docs.io

Built with commit 1dfd9ce.
This pull request is being automatically deployed with vercel-action

Copy link

github-actions bot commented Apr 7, 2025

Deploy preview for dagit-core-storybook ready!

✅ Preview
https://dagit-core-storybook-hrvxd0so6-elementl.vercel.app
https://dish-fe-842-try-to-improve.core-storybook.dagster-docs.io

Built with commit 1dfd9ce.
This pull request is being automatically deployed with vercel-action

@hellendag hellendag requested review from bengotow and salazarm April 7, 2025 21:51
@hellendag hellendag marked this pull request as ready for review April 7, 2025 21:51
@@ -28,10 +28,10 @@ interface ActiveSuggestionInfo {
idx: number;
}

export interface TokenizingFieldValue {
export type TokenizingFieldValue = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a few changes like this to ensure that the type arguments specified for useQueryPersistedState etc can satisfy QueryPersistedDataType.

if (Array.isArray(openNodes)) {
return new Set(openNodes.map((node) => String(node)));
}
return new Set();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is our bug case. We now explicitly check that the value provided via qs is an array, and cast the contents to string to match the expected return type.

@@ -239,7 +244,7 @@ describe('useQueryPersistedState', () => {
});
await userEvent.click(screen.getByText(`{"enableA":false,"enableB":true}`));
await waitFor(() => {
expect(querySearch).toEqual('');
expect(querySearch).toEqual('?enableA=true&enableB=false');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this was expecting empty string before...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is interesting actually - I think the reason is because the values were being set back to the default specified on like 225, and default values were omitted from the query string. I wonder if the defaults are sometimes used as the query-ified form and sometimes used as the state-form, so changing them to false above broke this?

| string
| undefined
| number
| boolean
| null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is to more or less match the recursiveness of what qs is able to encode, and clean up these any types.

Copy link
Contributor

@salazarm salazarm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@hellendag hellendag force-pushed the dish/fe-842-try-to-improve branch 2 times, most recently from 35d82c0 to 76467fa Compare April 10, 2025 15:59
Copy link
Collaborator

@bengotow bengotow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh this is great, I am definitely guilty of not thinking of the string[] vs string problem...

@@ -239,7 +244,7 @@ describe('useQueryPersistedState', () => {
});
await userEvent.click(screen.getByText(`{"enableA":false,"enableB":true}`));
await waitFor(() => {
expect(querySearch).toEqual('');
expect(querySearch).toEqual('?enableA=true&enableB=false');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is interesting actually - I think the reason is because the values were being set back to the default specified on like 225, and default values were omitted from the query string. I wonder if the defaults are sometimes used as the query-ified form and sometimes used as the state-form, so changing them to false above broke this?

@hellendag hellendag force-pushed the dish/fe-842-try-to-improve branch from 76467fa to 24931c8 Compare April 25, 2025 14:10
Comment on lines +96 to +97
.filter((s): s is AssetPartitionStatus =>
DISPLAYED_STATUSES.includes(s as AssetPartitionStatus),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard function contains a type assertion (s as AssetPartitionStatus) inside the guard itself, which undermines the purpose of the type guard. This pattern could allow invalid values to pass type checking if the assertion succeeds but the actual value isn't a valid AssetPartitionStatus.

Consider refactoring to a more reliable approach, such as:

.filter((s): s is AssetPartitionStatus => 
  typeof s === 'string' && DISPLAYED_STATUSES.includes(s as AssetPartitionStatus)
)

Or using a string literal union check if AssetPartitionStatus is an enum or string union type.

Suggested change
.filter((s): s is AssetPartitionStatus =>
DISPLAYED_STATUSES.includes(s as AssetPartitionStatus),
.filter((s): s is AssetPartitionStatus =>
typeof s === 'string' && DISPLAYED_STATUSES.includes(s as AssetPartitionStatus),

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

[INTERNAL_BRANCH=dish/qs-in-cloud]
@hellendag hellendag force-pushed the dish/fe-842-try-to-improve branch from 24931c8 to 1dfd9ce Compare April 25, 2025 15:02
Copy link
Member Author

hellendag commented Apr 25, 2025

Merge activity

  • Apr 25, 10:45 AM CDT: A user started a stack merge that includes this pull request via Graphite.
  • Apr 25, 10:45 AM CDT: @hellendag merged this pull request with Graphite.

@hellendag hellendag merged commit 4a43a7c into master Apr 25, 2025
7 of 8 checks passed
@hellendag hellendag deleted the dish/fe-842-try-to-improve branch April 25, 2025 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants