Я пытаюсь сделать какой-то синтаксический анализатор, который анализирует поля данных и превращает их в полную форму, а также отображает ее. Свойство fields
будет определять каждое поле в массиве данных json, которое клиент получит от свойства url
в IForm
Пример интерфейса формы выглядит следующим образом:
export interface IForm {
name: string;
url: string;
fields: IField<any>[] // <-- What is the proper type of this?
}
export interface IField<T> {
name: string;
label: string;
mandatory?: boolean;
default: T;
}
export interface IInput<T> extends IField<T> {
type: 'input'
}
export interface IOptionsUrl {
url: string;
idField: string;
labelField: string;
}
export interface IOptionsList<T> {
id: T;
label: string;
}
export interface IOptions<T> extends IField<IOptionsList<T>> {
type: 'options';
options?: IOptionsUrl | IOptionsList<T>[] | string[];
multiple?: boolean;
}
export interface ICheckbox extends IField<boolean> {
type: 'checkbox'
}
Свойство fields
в IForm
будет содержать несколько типов, например IField<string>
или IField<number>
и так далее. Они определяются свойством type
в каждом типе, все основано на IField. Поэтому я не уверен, стоит ли мне ставить <any>
, поскольку он будет содержать несколько типов данных в массиве. Каков правильный способ его определения? Или я должен пропустить все дженерики и просто использовать любой?
Примерные данные будут такими:
let meta: IForm = {
name: 'employee',
url: '/api/employee',
fields: [
{
type: 'input',
name: 'id',
label: 'Employee ID',
default: 0,
mandatory: true
},
{
type: 'input',
name: 'name',
label: 'Employee Name',
default: '',
mandatory: true
},
{
type: 'options',
name: 'gender',
label: 'Male/Female',
default: 'male',
options: ['Male', 'Female']
},
{
type: 'checkbox',
name: 'active',
label: 'Active',
default: true
},
{
type: 'options',
name: 'department',
label: 'Department',
default: 0,
options: {
url: '/api/departments',
idField: 'id',
labelField: 'name'
}
}
]
}
С интерфейсом сотрудника будет:
export interface IEmployee {
id: number;
name: string;
gender: string;
active: boolean;
department: number;
}
Как мне определить интерфейс для IForm?
Спасибо
Да, но <any> будет содержать все эти строки, числа, логические значения и дату одновременно в одном массиве.
any
охватывает более, а не только эти 4, поэтому, если вы хотите обеспечить безопасность типов, не используйте any
.
Как вы собираетесь использовать IForm
? Какие гарантии вы ожидаете от его fields
свойства? Может быть, вы можете отредактировать это в минимальный воспроизводимый пример, чтобы кто-то мог посоветовать вам, что делать. Существуют более строго типизированные решения, в которых IForm
сам по себе является общим, но сложность может не потребоваться в зависимости от ваших вариантов использования.
@jcalz только что добавил образцы данных.
@ErikPhilips Это я знаю. Поэтому я спрашиваю здесь, как я могу указать это, кроме как просто <any>
.
Я предполагаю, что вариант использования, который меня интересует, это то, что вы будете делать с meta
; в частности свойство meta.fields
. Вам нужно, чтобы компилятор помнил, что meta.fields[2].options
существует, не проверяя его сначала? Или вы планируете перебирать каждый элемент meta.fields
и делать разные вещи в зависимости от его свойства type
. В качестве первого предположения я бы сказал, что вы должны скомпилировать объединение возможных фактических типов IField<T>
, которые вы будете использовать (например, ICheckBox | IInput<string | number | boolean> | IOptions<string | number | boolean>
), и использовать их массив для типа fields
.
@jcalz Понятно.. Спасибо. Думаю, это было бы более уместно. Да, я планирую перебрать каждый элемент и показать правильную форму для этого поля. Параметр meta.fields[2] создаст список из 2 параметров в поле со списком, когда он отображается. Мужчина и женщина. URL-адрес в meta.fields[4] покажет то же самое, но будет получен из `/api/department' для своего списка.
@jcalz, можешь ли ты поставить это как ответ, чтобы я мог его принять?
Если вы хотите ограничить типы, используйте Тип Союза:
type formType = string | number | boolean | date;
export interface IForm {
name: string;
url: string;
fields: IField<formType>[] // <-- What is the proper type of this?
}
let form.default = "adsf"; //valid
let form.default = 1; //valid
let form.default = true; //valid
let form.default = new date(); //valid
let form.default = null; //in-valid
let form.default = undefined; //in-valid
let form.default = never; //in-valid
Итак, могу ли я определить поля: IFields<string> в первом элементе, затем IFields<number> во втором, затем IFields<boolean> в третьем с этим?
Можете ли вы исправить опечатки в этом коде? Он не будет компилироваться. (date
предположительно должно быть Date
, и я действительно не знаю, что должно быть let form.default =
...)
Я бы посоветовал, по крайней мере, с учетом приведенной выше информации, сделать что-то вроде этого:
type PossibleDataTypes = string | number | boolean; // or whatever you want
type PossibleFields =
| IInput<PossibleDataTypes>
| IOptions<PossibleDataTypes>
| ICheckbox;
export interface IForm {
name: string;
url: string;
fields: Array<PossibleFields>;
}
Здесь мы сужаем field
до массива только тех типов полей, которые вы ожидаете. Вы можете дополнить этот список, если хотите.
Кстати, я сделал еще одно изменение:
// changed this from IField<IOptionsList<T>> to just IOptionsList<T>
export interface IOptions<T> extends IField<T> {
type: "options";
options?:
| IOptionsUrl
| ReadonlyArray<IOptionsList<T>>
| ReadonlyArray<string>;
multiple?: boolean;
}
потому что ваша переменная meta
не соответствует без нее. Также я думаю, что в meta
была опечатка, где использовалось data
вместо url
. В любом случае вы можете определить meta
как IForm
, как вы это сделали, но расширение переменной до IForm
заставит забыть о деталях (например, какие конкретные типы полей вы используете). Если вы просто хотите проверять, который meta
соответствует IForm
, не расширяя его до IForm
, вы можете использовать такую вспомогательную функцию:
const asIForm = <F extends IForm>(f: F) => f;
А затем используйте его как
const meta = asIForm({
name: "employee",
url: "/api/employee",
fields: [
{
type: "input",
name: "id",
label: "Employee ID",
default: 0,
mandatory: true
},
{
type: "options",
name: "gender",
label: "Male/Female",
default: "male",
options: ["Male", "Female"]
},
{
type: "checkbox",
name: "active",
label: "Active",
default: true
},
{
type: "options",
name: "department",
label: "Department",
default: 0,
options: {
url: "/api/departments",
idField: "id",
labelField: "name"
}
}
]
});
Теперь, учитывая тот факт, что PossibleFields
является конкретным дискриминированный союз, вы можете заставить компилятор сузить каждую запись field
с помощью защиты типа, например:
function processForm(form: IForm) {
for (let field of form.fields) {
switch (field.type) {
case "input": {
// do something for input
break;
}
case "checkbox": {
// do something for checkbox
break;
}
case "options": {
// do something for options
field.options // <-- no error, known to exist
break;
}
default:
((x: never) => console.info("WHAT IS" + x))(field); // guarantee exhaustive
// If an error appears here -------------> ~~~~~
// then you missed a case in the switch statement
}
}
}
Хорошо, надеюсь, это поможет вам. Удачи!
Вау.. Большое спасибо.. Это бы многое исправило. Я не использую Date, так как у меня есть конкретный IDate для даты с диапазонами и ICheckbox для логического значения. Но мне этого достаточно для работы. Спасибо.
IField<any>[]
иArray<IField<any>>
являются синонимами в соответствии с документацией Array. Вы можете создать свой собственный Тип союза / Псевдоним типа, напримерtype formType = string | number | boolean | date;
. Тогда используйтеIField<formType>
.