Skip to content
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

feat(data-table): type using generics #1954

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

feat(data-table): type using generics #1954

wants to merge 12 commits into from

Conversation

metonym
Copy link
Collaborator

@metonym metonym commented Apr 20, 2024

This PR is based on #816.

  • Ugprades sveld to v0.20 to use generics
  • Uses generics to type DataTable so that types for rows and headers can be inferred

@brunnerh Would love your thoughts on this. Seems like a straightforward win. I love the inferred types based on keys in rows. FWIW, this doesn't break existing usage (see the DataTable.test.svelte file) of exported props, since the generic parameter is optional (<Row = DataTableRow>).

Screenshot 2024-04-20 at 4 15 18 PM Screenshot 2024-04-20 at 4 15 30 PM

* @typedef {string} DataTableKey
* @generics {Row extends DataTableRow = DataTableRow} Row
* @template {DataTableRow} Row
* @typedef {Exclude<keyof Row, "id">} DataTableKey<Row=DataTableRow>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The key type must disallow "id" being used as value, as that is a reserved prop for rows.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see a reason why the id would not also be displayable as a column?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point

@metonym
Copy link
Collaborator Author

metonym commented Apr 20, 2024

If using empty table headers, an explicit key must be set on rows:

Screenshot 2024-04-20 at 4 28 10 PM

@brunnerh
Copy link
Contributor

I have listed a few issues here that come up when typing keys like this. Looks like you also ran into one with having to add a property for an empty column.

There also can be columns that aggregate data from multiple properties or columns accessing nested properties (more detail on that in the linked comment).

@metonym
Copy link
Collaborator Author

metonym commented Apr 20, 2024

@brunnerh Thanks for the fast reply and the feedback. Looking at this more closely (especially nested keys and empty columns), this seems less feasible.

@brunnerh
Copy link
Contributor

brunnerh commented Apr 21, 2024

I think supporting nesting up to a low level (maybe just one level deeper) would be OK.
Maybe adding another generic to the header to bail out more easily would be good (so it defaults to determining valid keys but can be set to string).

Also for empty columns, the key type could potentially be changed based on the empty property 🤔.

(I can maybe look at this more closely and play around with it at another point.)

@brunnerh
Copy link
Contributor

Some observations:
I don't think that synthetic keys are an issue for the headers definitions in the <script>; usually either the headers will be more loosely typed in JS or one is using TS directly in which case 'key' as any can be used in those cases.

Strict typing of the cell slot prop key is not so great, though.
Comparisons with strings that are not among the known values will cause an error ("This comparison appears to be unintentional because the types '...' and '...' have no overlap."). In Svelte 5, TypeScript syntax will be possible in the template but right now this could be annoying to deal with.
The property could be typed in way that you still get valid property suggestions.

Upon further thought, changing any types for the empty columns does not make much sense to me, the cells behave essentially the same, just the header is different.

Property path resolution could be integrated but would likely require a separate .d.ts.
With one level deep:

// item type:
{
    id: number;
    name: {
        first: string;
        last: string;
    };
    deep: {
        deeper: {
            deepest: string; // <= not in suggestions
        };
    };
    age: number;
}

image

Experimental changes: 2b6c7b0

The path resolution could probably be extended to also resolve the type of cell.value and the like.

I would say the most important thing when adding generics to this component is the typing of the row slot property which allows safe access of all the data in the items. (This is already working nicely.)


Another thing that could be considered would be to use a generic just for the row keys which is based on the item type but also gets loosened to all strings. This type then could be used in the cell.key to get all keys that were actually defined as part of the headers; the key access in the template then would be as strict and correct as it possibly can be.

(sveld does not appear to support multiple generics at the moment, though.)

@metonym
Copy link
Collaborator Author

metonym commented Apr 28, 2024

@brunnerh That's awesome – thank you for your work on this. Agreed that the rows being typed is most important.

Would you mind opening a PR against this one with your change?

@metonym
Copy link
Collaborator Author

metonym commented Apr 28, 2024

sveld does not appear to support multiple generics at the moment, though.

It's a bit wonky, but for multiple generics, the values cannot be space-separated:

/**
 * @typedef {{ key: T1; value: T2; }} Typedef<T1,T2>
 * @generics {T1, T2} T1,T2
 */

brunnerh and others added 5 commits May 4, 2024 11:24
Adjust DataTable types.
- Make key in `cell` slot prop less strict to prevent type errors in markup.
- Resolve property path names up to one level deep for header keys.
@@ -5,7 +5,7 @@
* @typedef {import('./DataTableTypes.d.ts').PropertyPath<Row>} DataTableKey<Row=DataTableRow>
* @typedef {any} DataTableValue
* @typedef {{
* key: DataTableKey<Row>;
* key: DataTableKey<Row> | string;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reasoning for this?

Though I also question the necessity for the split into two header types, it feels like there could just be one with an optional value property.

(To retain auto completions on properties, this should be | (string & {}) as on the DataTableCell key.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Amended per your suggestion in 3407684

It was to allow the keys of empty table headers to not be included in rows. See cd90958

 const headers = [
    { key: "name", value: "Name" },
    { key: "port", value: "Port" },
    { key: "rule", value: "Rule" },
    { key: "overflow", empty: true }, // Keys in empty table headers should not be required as properties in `row`
  ] as const;

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it is a bit more common that columns without headers have some special content and are less likely to be bound to a specific property.

@metonym
Copy link
Collaborator Author

metonym commented May 4, 2024

@brunnerh Thank you for the help. Let me know if my additional commits look good.

I think this is a potential "breaking change" for TS users (although arguably it's a fix since it's now enforcing types).

Regarding adoption, my preferred recommendation for TS users is to use a const assertion for headers. An alternative would be to pass headers inline. I hesitate to recommend directly using the DataTableHeader<typeof row[0]> approach as I feel it is less elegant and more verbose.

1. Use a const assertion

headers expects a read-only array.

const headers = [
  { key: "name", value: "Name" },
  { key: "port", value: "Port" },
  { key: "rule", value: "Rule" },
] as const;

2. or pass headers inline

<DataTable
  headers={[
    { key: "name", value: "Name" },
    { key: "port", value: "Port" },
    { key: "rule", value: "Rule" },
  ]}
/>

@metonym metonym requested a review from brunnerh May 4, 2024 21:54
Copy link
Contributor

@brunnerh brunnerh left a comment

Choose a reason for hiding this comment

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

Looks good, I'd say.

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.

None yet

3 participants