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

[data grid] Why is GridValueFormatter defaulting the value to never? #12914

Open
j3ski-bioraptor opened this issue Apr 26, 2024 · 5 comments
Open
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature support: commercial Support request from paid users support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/ support: question Community support but can be turned into an improvement typescript

Comments

@j3ski-bioraptor
Copy link

j3ski-bioraptor commented Apr 26, 2024

The problem in depth

I have an issue while migrating from 6.x to 7.x.

In a lot of places I have the valueFormatter method configured as such:

{
  // other column configuration
  valueFormatter: ({ value }) => someValueFormatter(value)
}

this needs to be changed to

{
  // other column configuration
  valueFormatter: value => someValueFormatter(value)
}

However in 7.x the generic type of value here defaults to never and Typescript will not complain when attempting to destructure this:
https://www.typescriptlang.org/play/?noUncheckedIndexedAccess=true&noUnusedLocals=true&noUnusedParameters=true&exactOptionalPropertyTypes=true&suppressImplicitAnyIndexErrors=true&noPropertyAccessFromIndexSignature=true&noImplicitOverride=true&noFallthroughCasesInSwitch=true#code/C4TwDgpgBAQgTgewNYQHYDECuqDGwCWCqUAvFABQCGcA5gFxSoQBuEcAlKQHxTML4ATAFBCcRAM7AoAM1QN4yNFlwEipCgG9gESQF9OJHhqFRTUManEIANhAB01hDXLbJ7IbqA

I've tried running codemods, but they did nothing.

BTW. I would like to know why never was chosen here instead of unknown, which would at least make it explode.

Your environment

`npx @mui/envinfo`
  System:
    OS: macOS 12.6
  Binaries:
    Node: 18.10.0 - ~/.nvm/versions/node/v18.10.0/bin/node
    npm: 8.19.2 - ~/.nvm/versions/node/v18.10.0/bin/npm
    pnpm: 7.8.0 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 124.0.6367.91
    Edge: 124.0.2478.51
    Safari: 16.0
  npmPackages:
    @emotion/react: ^11.11.1 => 11.11.1
    @emotion/styled: ^11.11.0 => 11.11.0
    @mui/base:  5.0.0-beta.13
    @mui/core-downloads-tracker:  5.15.15
    @mui/icons-material: ^5.14.7 => 5.14.7
    @mui/lab: ^5.0.0-alpha.142 => 5.0.0-alpha.142
    @mui/material: ^5.15.15 => 5.15.15
    @mui/private-theming:  5.15.14
    @mui/styled-engine:  5.15.14
    @mui/system:  5.15.15
    @mui/types:  7.2.14
    @mui/utils:  5.15.14
    @mui/x-data-grid:  7.3.0
    @mui/x-data-grid-pro: ^7.3.0 => 7.3.0
    @mui/x-date-pickers: ^6.16.3 => 6.16.3
    @mui/x-license:  7.2.0
    @types/react: ^18.0.15 => 18.0.15
    react: ^18.2.0 => 18.2.0
    react-dom: ^18.2.0 => 18.2.0
    typescript: ^5.1.6 => 5.1.6.
tsconfig
{
  "compilerOptions": {
    "jsx": "preserve",
    "target": "ESNext",
    "module": "esnext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "typeRoots": ["node_modules/@types"],
    "types": ["vite/client"],
    "baseUrl": "src"
  },
  "include": ["src"]
}

Search keywords: GridValueFormatter
Order ID: 86856

@j3ski-bioraptor j3ski-bioraptor added status: waiting for maintainer These issues haven't been looked at yet by a maintainer support: commercial Support request from paid users labels Apr 26, 2024
@zannager zannager added support: question Community support but can be turned into an improvement component: data grid This is the name of the generic UI component, not the React module! support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/ labels Apr 26, 2024
@michelengelen
Copy link
Member

@j3ski-bioraptor thanks for raising this.
Is there any reason we are not using unknown here as well (although I did not have a problem with that yet).

Here is diff for changing the valueFormatter value type to unknown:

diff --git a/packages/x-data-grid/src/models/colDef/gridColDef.ts b/packages/x-data-grid/src/models/colDef/gridColDef.ts
index 36d7619a5..f22ee3291 100644
--- a/packages/x-data-grid/src/models/colDef/gridColDef.ts
+++ b/packages/x-data-grid/src/models/colDef/gridColDef.ts
@@ -57,7 +57,7 @@ export type GridValueFormatter<
   R extends GridValidRowModel = GridValidRowModel,
   V = any,
   F = V,
-  TValue = never,
+  TValue = unknown,
 > = (
   value: TValue,
   row: R,

Any objections @mui/xgrid ?

@michelengelen michelengelen changed the title [question] Why is GridValueFormatter defaulting the value to never? [data grid] Why is GridValueFormatter defaulting the value to never? Apr 26, 2024
@michelengelen michelengelen added typescript enhancement This is not a bug, nor a new feature and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Apr 26, 2024
@cherniavskii
Copy link
Member

@j3ski-bioraptor

BTW. I would like to know why never was chosen here instead of unknown, which would at least make it explode.

In v6 of the Data Grid, the value was typed as any.
Initially, I kept the same type, but when migrating to v7 TS wouldn't complain about it:

// TS should complain here, the first argument in v7 is the `value` itself, not the `params` object
valueFormatter: (params) => params.value,

The decision to use never was to get a TS error in the use case above.
It's not perfect though, because TS doesn't show the error if you destructure params.

We didn't use unknown because it's impossible to override on the valueFormatter level.
With never you can do this:

valueFormatter: (value: string, row) => value.toUpperCase(),

It's a trade-off that made sense to me at that time.
What do you think?

@michelengelen
Copy link
Member

OK, @cherniavskii this took me a while, but I think I might have a solution for this:

diff --git a/packages/x-data-grid/src/models/colDef/gridColDef.ts b/packages/x-data-grid/src/models/colDef/gridColDef.ts
index 36d7619a5..25066688c 100644
--- a/packages/x-data-grid/src/models/colDef/gridColDef.ts
+++ b/packages/x-data-grid/src/models/colDef/gridColDef.ts
@@ -23,6 +23,21 @@ export type GridAlignment = 'left' | 'right' | 'center';

 export type ValueOptions = string | number | { value: any; label: string } | Record<string, any>;

+type ValueMap = {
+  number: number;
+  string: string;
+  date: Date;
+  dateTime: Date;
+  boolean: boolean;
+  actions: never;
+  singleSelect: ValueOptions;
+  custom: never;
+};
+
+type ValueBasedType<T extends GridColType | undefined> = T extends GridColType
+  ? ValueMap[T]
+  : never;
+
 /**
  * Value that can be used as a key for grouping rows
  */
@@ -170,35 +185,10 @@ export interface GridBaseColDef<R extends GridValidRowModel = GridValidRowModel,
    * @returns {GridComparatorFn<V>} The comparator function to use.
    */
   getSortComparator?: (sortDirection: GridSortDirection) => GridComparatorFn<V> | undefined;
-  /**
-   * The type of the column.
-   * @default 'string'
-   * @see See {@link https://mui.com/x/react-data-grid/column-definition/#column-types column types docs} for more details.
-   */
-  type?: GridColType;
   /**
    * Allows to align the column values in cells.
    */
   align?: GridAlignment;
-  /**
-   * Function that allows to get a specific data instead of field to render in the cell.
-   */
-  valueGetter?: GridValueGetter<R, V, F>;
-  /**
-   * Function that allows to customize how the entered value is stored in the row.
-   * It only works with cell/row editing.
-   * @returns {R} The row with the updated field.
-   */
-  valueSetter?: GridValueSetter<R, V, F>;
-  /**
-   * Function that allows to apply a formatter before rendering its value.
-   */
-  valueFormatter?: GridValueFormatter<R, V, F>;
-  /**
-   * Function that takes the user-entered value and converts it to a value used internally.
-   * @returns {V} The converted value to use internally.
-   */
-  valueParser?: GridValueParser<R, V, F>;
   /**
    * Class name that will be added in cells for that column.
    */
@@ -289,65 +279,131 @@ export interface GridBaseColDef<R extends GridValidRowModel = GridValidRowModel,
    * @default 1
    */
   colSpan?: number | GridColSpanFn<R, V, F>;
-}
-
-/**
- * Column Definition interface used for columns with the `actions` type.
- * @demos
- *   - [Special column properties](/x/react-data-grid/column-definition/#special-properties)
- */
-export interface GridActionsColDef<R extends GridValidRowModel = any, V = any, F = V>
-  extends GridBaseColDef<R, V, F> {
   /**
    * The type of the column.
-   * @default 'actions'
-   */
-  type: 'actions';
-  /**
-   * Function that returns the actions to be shown.
-   * @param {GridRowParams} params The params for each row.
-   * @returns {React.ReactElement<GridActionsCellItemProps>[]} An array of [[GridActionsCell]] elements.
+   * @default 'string'
+   * @see See {@link https://mui.com/x/react-data-grid/column-definition/#column-types column types docs} for more details.
    */
-  getActions: (params: GridRowParams<R>) => React.ReactElement<GridActionsCellItemProps>[];
+  type?: GridColType;
 }

-/**
- * Column Definition interface used for columns with the `singleSelect` type.
- * @demos
- *   - [Special column properties](/x/react-data-grid/column-definition/#special-properties)
- */
-export interface GridSingleSelectColDef<R extends GridValidRowModel = any, V = any, F = V>
-  extends GridBaseColDef<R, V, F> {
+export interface GridValueMethods<
+  T extends GridColType,
+  R extends GridValidRowModel = GridValidRowModel,
+  V = any,
+  F = V,
+> {
   /**
-   * The type of the column.
-   * @default 'singleSelect'
+   * Function that allows to get a specific data instead of field to render in the cell.
    */
-  type: 'singleSelect';
+  valueGetter?: GridValueGetter<R, V, F, ValueBasedType<T>>;
   /**
-   * To be used in combination with `type: 'singleSelect'`. This is an array (or a function returning an array) of the possible cell values and labels.
+   * Function that allows to customize how the entered value is stored in the row.
+   * It only works with cell/row editing.
+   * @returns {R} The row with the updated field.
    */
-  valueOptions?: Array<ValueOptions> | ((params: GridValueOptionsParams<R>) => Array<ValueOptions>);
+  valueSetter?: GridValueSetter<R, V, F>;
   /**
-   * Used to determine the label displayed for a given value option.
-   * @param {ValueOptions} value The current value option.
-   * @returns {string} The text to be displayed.
+   * Function that allows to apply a formatter before rendering its value.
    */
-  getOptionLabel?: (value: ValueOptions) => string;
+  valueFormatter?: GridValueFormatter<R, V, F, ValueBasedType<T>>;
   /**
-   * Used to determine the value used for a value option.
-   * @param {ValueOptions} value The current value option.
-   * @returns {string} The value to be used.
+   * Function that takes the user-entered value and converts it to a value used internally.
+   * @returns {V} The converted value to use internally.
    */
-  getOptionValue?: (value: ValueOptions) => any;
+  valueParser?: GridValueParser<R, V, F>;
 }

+/**
+ * Column Definition interface used for columns with the `actions` type.
+ * @demos
+ *   - [Special column properties](/x/react-data-grid/column-definition/#special-properties)
+ */
+export type GridActionsColDef<R extends GridValidRowModel = any, V = any, F = V> = GridBaseColDef<
+  R,
+  V,
+  F
+> &
+  GridValueMethods<'actions', R, V, F> & {
+    /**
+     * The type of the column.
+     * @default 'actions'
+     */
+    type: 'actions';
+    /**
+     * Function that returns the actions to be shown.
+     * @param {GridRowParams} params The params for each row.
+     * @returns {React.ReactElement<GridActionsCellItemProps>[]} An array of [[GridActionsCell]] elements.
+     */
+    getActions: (params: GridRowParams<R>) => React.ReactElement<GridActionsCellItemProps>[];
+  };
+
+/**
+ * Column Definition interface used for columns with the `singleSelect` type.
+ * @demos
+ *   - [Special column properties](/x/react-data-grid/column-definition/#special-properties)
+ */
+export type GridSingleSelectColDef<
+  R extends GridValidRowModel = any,
+  V = any,
+  F = V,
+> = GridBaseColDef<R, V, F> &
+  GridValueMethods<'singleSelect', R, V, F> & {
+    /**
+     * The type of the column.
+     * @default 'singleSelect'
+     */
+    type: 'singleSelect';
+    /**
+     * To be used in combination with `type: 'singleSelect'`. This is an array (or a function returning an array) of the possible cell values and labels.
+     */
+    valueOptions?:
+      | Array<ValueOptions>
+      | ((params: GridValueOptionsParams<R>) => Array<ValueOptions>);
+    /**
+     * Used to determine the label displayed for a given value option.
+     * @param {ValueOptions} value The current value option.
+     * @returns {string} The text to be displayed.
+     */
+    getOptionLabel?: (value: ValueOptions) => string;
+    /**
+     * Used to determine the value used for a value option.
+     * @param {ValueOptions} value The current value option.
+     * @returns {string} The value to be used.
+     */
+    getOptionValue?: (value: ValueOptions) => any;
+  };
+
+export type GridTypedValueColDef<
+  R extends GridValidRowModel = any,
+  V = any,
+  F = V,
+> = GridBaseColDef<R, V, F> &
+  (
+    | ({
+        type?: 'string';
+      } & GridValueMethods<'string', R, V, F>)
+    | ({
+        type: 'number';
+      } & GridValueMethods<'number', R, V, F>)
+    | ({
+        type: 'date' | 'dateTime';
+      } & GridValueMethods<'date', R, V, F>)
+    | ({
+        type: 'boolean';
+      } & GridValueMethods<'boolean', R, V, F>)
+    | ({
+        type: 'custom';
+      } & GridValueMethods<'custom', R, V, F>)
+  );
+
 /**
  * Column Definition interface.
  * @demos
  *   - [Column definition](/x/react-data-grid/column-definition/)
  */
 export type GridColDef<R extends GridValidRowModel = any, V = any, F = V> =
-  | GridBaseColDef<R, V, F>
+  | GridTypedValueColDef<R, V, F>
   | GridActionsColDef<R, V, F>
   | GridSingleSelectColDef<R, V, F>;

I guess this could be improved a bit still, but it works as long as you type the columns as we do normally in our docs as well:

Screenshot 2024-04-29 at 12 01 49

WDYT?

@cherniavskii
Copy link
Member

cherniavskii commented Apr 29, 2024

@michelengelen This is based on the assumption that the value type corresponds to the column type, correct?
If so, it's not really helpful for valueGetter since it's mostly used when the value type does not match the column type:

{
  field: 'dateString',
  type: 'date',
  // convert string to Date
  valueGetter: (value) => new Date(value),
}

Furthermore, there's no way to override the assumed type if necessary.

I would rather look into the TypeScript builder approach where we can use generic type variables to infer the actual type based on the field

@michelengelen
Copy link
Member

@michelengelen This is based on the assumption that the value type corresponds to the column type, correct? If so, it's not really helpful for valueGetter since it's mostly used when the value type does not match the column type:

{
  field: 'dateString',
  type: 'date',
  // convert string to Date
  valueGetter: (value) => new Date(value),
}

So valueGetter should actually use TValue = any and return the correct type based on the column type, right?

So something like this:

export type GridValueGetter<
  R extends GridValidRowModel = GridValidRowModel,
  V = any,
  F = V,
  TValue = any,
> = (
  value: any,
  row: R,
  column: GridColDef<R, V, F>,
  apiRef: React.MutableRefObject<GridApiCommunity>,
) => TValue;

valueGetter?: GridValueGetter<R, V, F, ValueBasedType<T>>;

But this feels somewhat wrong.

Furthermore, there's no way to override the assumed type if necessary.

Overridability can be restored. No problem.

I would rather look into the TypeScript builder approach where we can use generic type variables to infer the actual type based on the field

This is an interesting approach. I have used type inference in the past and it led to some nasty errors at that time. Maybe this got better during the latest typescript releases. I will test this in the codebase!

Thanks for the suggestions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature support: commercial Support request from paid users support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/ support: question Community support but can be turned into an improvement typescript
Projects
None yet
Development

No branches or pull requests

4 participants