【问题标题】:Multiple generics in one interface一个接口中的多个泛型
【发布时间】:2019-06-11 00:35:53
【问题描述】:

我正在尝试制作某种解析器,用于解析数据字段,并将其制成完整的形式并进行显示。 fields 属性将定义客户端将从url 属性IForm 接收的json 数据数组中的每个字段

表单界面示例如下:

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'
}

IFormfields 属性将包含多个类型,例如IField&lt;string&gt;IField&lt;number&gt; 等。它们由每种类型中的type 属性确定,全部基于 IField。所以我不确定是否应该输入&lt;any&gt;,因为它将在数组中包含多种类型的数据。定义它的正确方法是什么?还是我应该一起跳过泛型而只使用任何泛型?

样本数据如下:

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 的接口?

谢谢

【问题讨论】:

  • IField&lt;any&gt;[]Array&lt;IField&lt;any&gt;&gt; are synonymous according to the Array documentation。您可以创建自己的Union Type / Type Alias,例如type formType = string | number | boolean | date;。然后使用IField&lt;formType&gt;
  • 是的,但是 将在一个数组中同时包含所有这些字符串、数字、布尔值和日期。
  • any 涵盖 更多 而不仅仅是这 4 个,所以如果你想要类型安全,请不要使用 any
  • 你打算如何使用IForm?您期望其fields 属性提供什么类型的保证?也许您可以将其编辑为minimal reproducible example,以便有人可以建议您做什么。还有更多的强类型解决方案,其中IForm 本身就是通用的,但根据您的用例,可能不需要复杂性。
  • @jcalz 刚刚添加了示例数据。

标签: typescript


【解决方案1】:

我建议,至少考虑到上述信息,您可以这样做:

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 是具体的discriminated union,您可以让编译器通过类型保护来缩小每个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.log("WHAT IS" + x))(field); // guarantee exhaustive
        // If an error appears here -------------> ~~~~~
        // then you missed a case in the switch statement
    }
  }
}

好的,希望对您有所帮助。祝你好运!

Link to code

【讨论】:

  • 哇.. 非常感谢.. 这可以解决很多问题。我不使用日期,因为我有一个特定的 IDate 日期范围和 ICheckbox 布尔值。但这对我来说已经足够了。谢谢。
【解决方案2】:

如果你想限制类型使用Type Union:

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 在第一个元素,然后 IFields 在第二个,然后 IFields 在第三个?
  • 你能修正这段代码中的错别字吗?它不会编译。 (date大概应该是Date,我真的不知道let form.default =应该是什么……)
猜你喜欢
  • 2018-07-23
  • 2021-10-09
  • 2019-04-06
  • 2020-12-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多