Я пытаюсь создать интерфейс с помощью общего метода. Я не знаю, как указать общий тип при использовании его в списке объектов.
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
@SaniHuttunen Это единственное решение?
Это единственный способ определить то, что вы просили: универсальный интерфейс.
• Пожалуйста, отредактируйте этот код, чтобы он был минимально воспроизводимым примером . Фактическая ошибка, которую вы получаете с <string>(cell: string) => ...
, заключается в том, что string
не может быть именем параметра типа. Непонятно, что вы там пытаетесь сделать, так может просто убрать весь этот кусок? Не используйте string
в качестве имени параметра универсального типа. • Каков вариант использования? Если у вас есть значение типа Column
, что вы можете сделать со свойством formatter
, если вы не знаете T
? Я мог бы представить себе древовидную структуру, представляющую столбец и его вложенные дочерние элементы, но это сложно. Пожалуйста, отредактируйте, чтобы объяснить, как вы собираетесь использовать этот тип.
@jcalz Я редактирую вопрос. думаю теперь более понятно
Вы удалили проблемный универсальный вариант, но я все еще не понимаю варианта использования. Учитывая listOfEmployeesColumns
типа Column[]
, что с ним можно сделать? Как можно использовать методы formatter
? Откуда вы знаете, какие типы параметров они принимают? Что будет, если я позвоню listOfEmployeesColumns.map(x => x.formatter?.(null))
? Вы получаете ошибки во время выполнения, потому что null
не является допустимым аргументом ни для одного из средств форматирования. Но откуда ТС это знать? Какой здесь вариант использования?
@jcalz Используется для изменения значения при создании значения ячейки в таблице. cell
ценности быть не может null
. Значение из data
передано в таблицу: const Table = ( { rows, columns, index = false, handleCreate = undefined }: { rows: Data, columns: Column[], index?: boolean, handleCreate?: () => void } )
Вы говорите, что этого не может быть null
, но сейчас нигде нет кода, подразумевающего это. В вашем примере в комментарии какой тип Data
? Это будет ключом к определению Column
, поскольку, видимо, от этого зависит. Пожалуйста, отредактируйте , чтобы код стал минимально воспроизводимым примером, который показывает, как он работает. Должно быть очевидно, как на самом деле используются смешанные/вложенные данные.
@jcalz я еще раз редактирую
Хм, я бы сказал, что вы действительно хотите, чтобы Column
были общими по типу rows
элементов, как показано в этой ссылке на игровую площадку. Соответствует ли это вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?
@jcalz Я бы хотел, чтобы в качестве общего использовался только метод formatter
, а не Column
. Это возможно?
Нет смысла делать этот метод универсальным. Универсальные методы позволяют вызывающей стороне выбирать аргумент универсального типа, а не разработчику. Так что не только нельзя, но и непонятно, что вы вообще под этим подразумеваете. Более того, разве index
не влияет на то, какой тип formatter
принимает? Действительно кажется, что вам нужна информация о параметрах типа в области Column
, а не только метода formatter
. Можете ли вы посмотреть ссылку на мою игровую площадку из моего предыдущего комментария и сообщить мне, работает ли она так, как вы ожидаете, или нет?
@jcalz да, этот пример правильный
Вы не определяете общий интерфейс.
Это должно быть что-то вроде:
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("-", "/")
}
]
Это будет работать, но в этом списке смешанные типы, а также вложенные столбцы могут быть других типов. Я хотел бы сделать только метод общим
Это невозможно сделать. Что вы могли бы сделать, так это заставить все «смешанные типы» наследовать от одного и того же базового класса и использовать базовый класс в качестве универсального типа. Но это не всегда возможно, если типы слишком разные.
Единственный способ, которым это может работать, — это если 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
, чего недостаточно для проверки соответствия listOfEmployeesColumns
listOfEmployees
.
Если мы напишем список встроенным, то нам не понадобится утверждение 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
.
export interface Column<T>