typescriptreact-typescriptreact-aria

How can I correctly infer the type of a context value?


I use React Aria Components' Table with context to select or deselect all the rows in the table.

However, I get Property 'selectedKeys' does not exist on type 'WithRef<TableProps, HTMLTableElement> | SlottedValue<WithRef<TableProps, HTMLTableElement>>'. Property 'selectedKeys' does not exist on type 'SlottedValue<WithRef<TableProps, HTMLTableElement>>'. with this code:

import { useContext } from "react";
import {
  TableContext,
  Table,
  TableHeader,
  TableBody,
  Row,
  Cell,
  Column,
} from "react-aria-components";
import { Checkbox } from "./Checkbox";

type User = {
  firstName: string;
  lastName: string;
  id: number;
};

export default function App() {
  const users: User[] = [
    { firstName: "John", lastName: "Doe", id: 1 },
    { firstName: "Jane", lastName: "Doe", id: 2 },
    { firstName: "Joe", lastName: "Doe", id: 3 },
  ];
  const tableContext = useContext(TableContext);
  const selectedKeysCount = tableContext?.selectedKeys.size || 0;

  const handleSelectionChange = () => {
    if (selectedKeysCount === users.length) {
      tableContext?.onSelectionChange(new Set());
      return;
    }

    const newSelectedKeys = new Set();
    users.forEach((user) => newSelectedKeys.add(user.id));
    tableContext?.onSelectionChange(newSelectedKeys);
  };

  return (
    <>
      <Checkbox
        slot="selection"
        isIndeterminate={
          selectedKeysCount > 0 && selectedKeysCount < users.length
        }
        isSelected={selectedKeysCount > 0 || false}
        onChange={handleSelectionChange}
      >
        Select all
      </Checkbox>

      <Table aria-label="Users" selectionMode="multiple">
        <TableHeader>
          <Column isRowHeader />
          <Column>First Name</Column>
          <Column>Last Name</Column>
        </TableHeader>
        <TableBody items={users}>
          {(item) => (
            <Row>
              <Cell>
                <Checkbox slot="selection" />
              </Cell>
              <Cell>{item.firstName}</Cell>
              <Cell>{item.lastName}</Cell>
            </Row>
          )}
        </TableBody>
      </Table>
    </>
  );
}

Here's a minimal reproduction sandbox.

My current solution is to sort of ignore the problem, but I'd like to understand why the type isn't inferred correctly and how I can tell typescript which type to use.

export default function App() {
  // ...

  const tableContext = useContext(TableContext);
  // @ts-expect-error TypeScript doesn't recognise `selectedKeys` on `tableContext`
  const selectedKeysCount = tableContext?.selectedKeys.size || 0;

  const handleSelectionChange = () => {
    if (selectedKeysCount === users.length) {
      // @ts-expect-error TypeScript doesn't recognise `onSelectionChange` on `tableContext`
      tableContext?.onSelectionChange(new Set());
      return;
    }

    const newSelectedKeys = new Set();
    users.forEach((user) => newSelectedKeys.add(user.id));
    // @ts-expect-error TypeScript doesn't recognise `onSelectionChange` on `tableContext`
    tableContext?.onSelectionChange(newSelectedKeys);
  };

  // ...
}

Solution

  • Got it to work by using RAC's useSlottedContext instead of useContext:

    import {
      // ...
      useSlottedContext,
    } from "react-aria-components";
    
    export default function App() {
      // ...
    
      const tableContext = useSlottedContext(TableContext);
      const selectedKeysCount = tableContext?.selectedKeys.size || 0;
    
      const handleSelectionChange = () => {
        if (selectedKeysCount === users.length) {
          tableContext?.onSelectionChange(new Set());
          return;
        }
    
        const newSelectedKeys = new Set();
        users.forEach((user) => newSelectedKeys.add(user.id));
        tableContext?.onSelectionChange(newSelectedKeys);
      };
    
      // ...
    }