Проблема с методом универсального интерфейса TypeScript

Я пытаюсь создать интерфейс с помощью общего метода. Я не знаю, как указать общий тип при использовании его в списке объектов.

export type Data = Array<object>
export interface Column {
    name: string
    index: string
    columns?: Column[]
    formatter?: <T>(cell: T) => T
}

export const listOfEmployeesColumns: Column[] = [
    {
        name: "Salary",
        index: "salary",
        formatter: (cell: number) =>  cell * 0.23
    },
    {
        name: "Hire date",
        index: "hire_date",
        formatter: (cell: string) =>  cell.replace("-", "/")
    }
]

export const listOfEmployees = [
    {
        "salary": 7000,
        "hire_date": "2020-01-15"
    },
    {
        "salary": 11000
        "hire_date": "2018-07-01"
    }
]

const Table = (
    { rows, columns, index = false, handleCreate = undefined }:
        { rows: Data, columns: Column[], index?: boolean, handleCreate?: () => void }
) => {
    return {columns.map(column => {
       return column.formatter !== null ? <td>column.formatter(rows[column.index])</td> : <td>rows[column.index]</td>
    })
}

<Table 
    rows = {listOfEmployees} 
    columns = {listOfEmployeesColumns}
/>

В этом случае я получил эту ошибку:

Type '(cell: string) => string' is not assignable to type '<T>(cell: T) => T'.
Types of parameters 'cell' and 'cell' are incompatible.
Type 'T' is not assignable to type 'string'.

Как указать общий тип метода форматирования интерфейса Column в списке объектов Column

export interface Column<T>
Sani Huttunen 25.08.2024 16:45

@SaniHuttunen Это единственное решение?

Tomasz Lipiński 25.08.2024 16:51

Это единственный способ определить то, что вы просили: универсальный интерфейс.

Sani Huttunen 25.08.2024 16:52

• Пожалуйста, отредактируйте этот код, чтобы он был минимально воспроизводимым примером . Фактическая ошибка, которую вы получаете с <string>(cell: string) => ..., заключается в том, что string не может быть именем параметра типа. Непонятно, что вы там пытаетесь сделать, так может просто убрать весь этот кусок? Не используйте string в качестве имени параметра универсального типа. • Каков вариант использования? Если у вас есть значение типа Column, что вы можете сделать со свойством formatter, если вы не знаете T? Я мог бы представить себе древовидную структуру, представляющую столбец и его вложенные дочерние элементы, но это сложно. Пожалуйста, отредактируйте, чтобы объяснить, как вы собираетесь использовать этот тип.

jcalz 25.08.2024 17:26

@jcalz Я редактирую вопрос. думаю теперь более понятно

Tomasz Lipiński 25.08.2024 21:08

Вы удалили проблемный универсальный вариант, но я все еще не понимаю варианта использования. Учитывая listOfEmployeesColumns типа Column[], что с ним можно сделать? Как можно использовать методы formatter? Откуда вы знаете, какие типы параметров они принимают? Что будет, если я позвоню listOfEmployeesColumns.map(x => x.formatter?.(null))? Вы получаете ошибки во время выполнения, потому что null не является допустимым аргументом ни для одного из средств форматирования. Но откуда ТС это знать? Какой здесь вариант использования?

jcalz 25.08.2024 21:11

@jcalz Используется для изменения значения при создании значения ячейки в таблице. cellценности быть не может null. Значение из data передано в таблицу: const Table = ( { rows, columns, index = false, handleCreate = undefined }: { rows: Data, columns: Column[], index?: boolean, handleCreate?: () => void } )

Tomasz Lipiński 25.08.2024 21:16

Вы говорите, что этого не может быть null, но сейчас нигде нет кода, подразумевающего это. В вашем примере в комментарии какой тип Data? Это будет ключом к определению Column, поскольку, видимо, от этого зависит. Пожалуйста, отредактируйте , чтобы код стал минимально воспроизводимым примером, который показывает, как он работает. Должно быть очевидно, как на самом деле используются смешанные/вложенные данные.

jcalz 25.08.2024 21:42

@jcalz я еще раз редактирую

Tomasz Lipiński 25.08.2024 21:47

Хм, я бы сказал, что вы действительно хотите, чтобы Column были общими по типу rows элементов, как показано в этой ссылке на игровую площадку. Соответствует ли это вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 25.08.2024 21:58

@jcalz Я бы хотел, чтобы в качестве общего использовался только метод formatter, а не Column. Это возможно?

Tomasz Lipiński 25.08.2024 22:01

Нет смысла делать этот метод универсальным. Универсальные методы позволяют вызывающей стороне выбирать аргумент универсального типа, а не разработчику. Так что не только нельзя, но и непонятно, что вы вообще под этим подразумеваете. Более того, разве index не влияет на то, какой тип formatter принимает? Действительно кажется, что вам нужна информация о параметрах типа в области Column, а не только метода formatter. Можете ли вы посмотреть ссылку на мою игровую площадку из моего предыдущего комментария и сообщить мне, работает ли она так, как вы ожидаете, или нет?

jcalz 25.08.2024 22:15

@jcalz да, этот пример правильный

Tomasz Lipiński 25.08.2024 22:17
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
13
54
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы не определяете общий интерфейс.
Это должно быть что-то вроде:

export interface Column<T> {
    name: string
    index: string
    columns?: Column<T>[]
    formatter?: (cell: T) => T
}

export const listOfEmployeesColumns: Column<string>[] = [
    {
        name: "Hire date",
        index: "hire_date",
        formatter: (cell: string) =>  cell.replace("-", "/")
    }
]

Это будет работать, но в этом списке смешанные типы, а также вложенные столбцы могут быть других типов. Я хотел бы сделать только метод общим

Tomasz Lipiński 25.08.2024 16:53

Это невозможно сделать. Что вы могли бы сделать, так это заставить все «смешанные типы» наследовать от одного и того же базового класса и использовать базовый класс в качестве универсального типа. Но это не всегда возможно, если типы слишком разные.

Sani Huttunen 25.08.2024 16:55
Ответ принят как подходящий

Единственный способ, которым это может работать, — это если Column является общим по типу T данных, которые он представляет (тип параметра и возвращаемое значение formatter). Вы не хотите, чтобы formatter был универсальным методом. Это означало бы, что вызывающая сторона может выбрать аргумент типа. Поскольку вам нужно, чтобы разработчик выбрал аргумент типа, это означает, что сам Column должен быть универсальным.

Кроме того, в соответствии с вашим вариантом использования, оно также по существу является универсальным по типу K свойства index, поскольку это свойство должно совпадать с ключом соответствующих данных... и T должно соответствовать типу свойства в этом ключе.

Имея это в виду, вот возможное определение Column:

interface Column<K extends PropertyKey, T> {
    name: string
    index: K
    columns?: T extends object ? readonly ColumnForProps<T>[] : never;
    formatter?: (cell: T) => T
}

type ColumnForProps<T extends object> =
    { [K in keyof T]-?: Column<K, T[K]> }[keyof T]

Обратите внимание, что это фактически рекурсивное определение. Свойство columns является условным типом . Если тип T сам по себе является объектом, то columns должен быть массивом объектов Column для каждого свойства T. (Я использую массив только для чтения , который более разрешителен, чем изменяемый массив.) Если T не является объектом, то columns является необязательным свойством невозможного никогда не вводить, что означает, что если T не является объектом, у вас не должно быть определенного свойства columns для Column<K, T>.

Тип ColumnForProps<T> — это тип распространяемого объекта (как он создан в microsoft/TypeScript#47109 ), который становится объединениемColumn<K, T[K]> для каждого K в keyof T. Итак, если T — это {a: A, b: B}, то ColumnForProps<T> — это Column<"a", A> | Column<"b", B>.


Это также означает, что Table должно быть общим по типу T элементов свойства rows, например:

const Table = <T extends object>(
    { rows, columns, index = false, handleCreate = undefined }: {
        rows: readonly T[],
        columns: readonly ColumnForProps<T>[],
        index?: boolean,
        handleCreate?: () => void
    }
) => (null!); // <-- not worried about implementation

Обратите внимание, что я удалил реализацию, поскольку она нас здесь не интересует, а реальная реализация, по-видимому, должна быть рекурсивной, и я не хочу отклоняться от темы.

Итак, когда вы вызываете Table, TypeScript должен вывести T из типа элемента свойства rows, а затем использовать его, чтобы потребовать, чтобы columns был массивом объектов ColumnForProps<T>.

Давайте проверим это:

const listOfEmployeesColumns = [
    {
        name: "Salary",
        index: "salary",
        formatter: (cell: number) => cell * 0.23
    },
    {
        name: "Hire date",
        index: "hire_date",
        formatter: (cell: string) => cell.replace("-", "/")
    }
] as const;

const listOfEmployees = [
    {
        "salary": 7000,
        "hire_date": "2020-01-15"
    },
    {
        "salary": 11000,
        "hire_date": "2018-07-01"
    }
];

<Table
    rows = {listOfEmployees} // okay
    columns = {listOfEmployeesColumns} // okay
/>

Это работает без ошибок. Обратите внимание, что мне пришлось использовать константное утверждение в инициализаторе listOfEmployeesColumns, чтобы TypeScript отслеживал литералы типов свойств index. В противном случае TypeScript выведет string, чего недостаточно для проверки соответствия listOfEmployeesColumnslistOfEmployees.

Если мы напишем список встроенным, то нам не понадобится утверждение const, потому что TypeScript знает, что нужно обращать внимание на литеральные типы index:

<Table
    rows = {listOfEmployees}
    columns = {[
        {
            name: "Salary",
            index: "salary",
            formatter(cell) { return cell - 1 }
        },
        {
            name: "Hire date",
            index: 'hire_date',
            formatter(cell) { return cell.toUpperCase() }
        }
    ]}
/>

Вы можете видеть, что TypeScript будет жаловаться, если вы попытаетесь использовать index, которого он не ожидает:

<Table
    rows = {listOfEmployees}
    columns = {[
        { name: "Salary", index: "salary" },
        { name: "Hire date", index: 'hire_date' },
        { name: "Oops", index: 'oops' } // error!
        //              ~~~~~ <-- Type '"oops"' is not 
        // assignable to type '"salary" | "hire_date"'.
    ]}
/>

Или, если вы поругаетесь formatter:

<Table
    rows = {listOfEmployees}
    columns = {[
        { name: "Salary", index: "salary" },
        {
            name: "Hire date", index: 'hire_date',
            formatter(cell: number) { return cell + 1 }
        }, // error!
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Types of property 'formatter' are incompatible.
    ]}
/>

Надеемся, что это должно оправдать необходимость того, чтобы Column было общим и для типа index, поскольку именно это свойство управляет ожидаемым типом formatter.

Наконец, поскольку Column рекурсивно, давайте посмотрим, что произойдет, если rows содержит вложенные объекты:

<Table
    rows = {[{ a: { b: "abc" } }, { a: { b: "def" } }]}
    columns = {[
        {
            index: "a", name: "A", columns: [
                { index: "b", name: "B", formatter(cell) { return cell.toLowerCase() } }
            ]
        }
    ]}
/>

Выглядит хорошо. Тип элементов rows{a: {b: string}}, и вы можете видеть, что TypeScript ожидает, что свойство columns элемента index: "a" будет столбцом index: "b", где formatter ожидает и возвращает значение string.

Детская площадка, ссылка на код

Другие вопросы по теме