Search code examples
typescriptdryreact-virtuoso

How to create a generic implementation of a generic interface in typescript?


I would like to apply DRY to the following situation. There is a generic interface "SomeInterface<Data = unknown>", and I need several implementations of that interface, which happen to be all equal except for the explicit type definition. I would like to have a generic class/interface/expression that collects all the commonalities.

For more clarity, I am using react-virtuoso and I want several TableComponents like the following. Instead of declaring VirtuosoTableComponents_booklists and VirtuosoTableComponents_books, I would like to have something like VirtuosoTableComponents<T> and then use it like components={VirtuosoTableComponents<booklists>}.

tables.tsx

import { Book, BookList } from './data-context';

const VirtuosoTableComponents_booklists: TableComponents<BookList> = {
    // eslint-disable-next-line react/display-name
    Scroller: React.forwardRef<HTMLDivElement>((props, ref) => <TableContainer component={Paper} {...props} ref={ref} />),
    Table: props => <Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />,
    TableHead,
    TableRow: props => <TableRow {...props} />,
    // eslint-disable-next-line react/display-name
    TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => <TableBody {...props} ref={ref} />)
};

const VirtuosoTableComponents_books: TableComponents<Book> = {
    // eslint-disable-next-line react/display-name
    Scroller: React.forwardRef<HTMLDivElement>((props, ref) => <TableContainer component={Paper} {...props} ref={ref} />),
    Table: props => <Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />,
    TableHead,
    TableRow: props => <TableRow {...props} />,
    // eslint-disable-next-line react/display-name
    TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => <TableBody {...props} ref={ref} />)
};



export const MyTables = () : React.JSX.Element => {
    const [data, updateData] = React.useState(undefined as unknown as CtxData);
    React.useEffect(() => {
        // .... fetch data ....
        updateData(response);
    }


    // function fixedHeaderContent_booklists ..
    // function fixedHeaderContent_books ..
    // function rowContent_booklists ..
    // function rowContent_books ..

    return (
    <>
        <DataContext.Provider value={data}>
            <div><p>Book Lists</p></div>
            <TableVirtuoso
                data={data.booklists.rows}
                components={VirtuosoTableComponents_booklists}
                fixedHeaderContent={fixedHeaderContent_booklists}
                itemContent={rowContent_booklists}
            />
            <div><p>Books</p></div>
            <TableVirtuoso
                data={data.books.rows}
                components={VirtuosoTableComponents_books}
                fixedHeaderContent={fixedHeaderContent_books}
                itemContent={rowContent_books}
            />
        </DataContext.Provider>
    </>
    )
}

data-context.ts

export interface BookList {
  bookRef: string[]
}

export interface Book {
  title: string;
  author: string;
}

export interface ColumnData<T> {
    dataKey: keyof T;
    label: string;
    width: number;
}

export interface CtxData {
    booklists: { rows: BookList[]; columns: ColumnData<BookList>[] };
    books: { rows: Book[]; columns: ColumnData<Book>[] };
}

I have tried extracting a new interface, but no success.


Solution

  • You could create a factory-like function that creates the TableComponents objects:

    function createTableComponents<T>(): TableComponents<T> {
        return {
            // eslint-disable-next-line react/display-name
            Scroller: React.forwardRef<HTMLDivElement>((props, ref) => <TableContainer component={Paper} {...props} ref={ref} />),
            Table: props => <Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />,
            TableHead,
            TableRow: props => <TableRow {...props} />,
            // eslint-disable-next-line react/display-name
            TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => <TableBody {...props} ref={ref} />)
        };
    };
    

    Which would allow you to do:

    const VirtuosoTableComponents_booklists = createTableComponents<BookList>();
    const VirtuosoTableComponents_books = createTableComponents<Book>();
    

    Would that work?