递归接口定义与泛型相结合

时间:2019-07-01 09:55:59

标签: typescript generics interface typescript-generics

我只是遇到了一些讨厌的嵌套数据,我想用Typescript接口来描述。

让我们首先看一下数据样本

const d = {
    callbacks: { 
        x: { cb: (data: number) => {} },
        y: { cb: (data: string) => {} }
    },
    foo: {
        callbacks: { 
            z: { cb: (data: boolean) => {} }
        }, 
        bar: { /* .... */ }
    },
    baz: { /* .... */ }
}

注意:每个级别都有一个callback(必需)键和一些随机键(例如foobar)。还请注意,函数的参数可以是任何东西!

我尝试为此数据结构创建接口的结果是:

interface DataItem<T> {
   cb: (data: T) => void;
}

interface Data {
   [key: string]: DataItem | Data;
}

Playground

enter image description here

现在我看到2个问题:

1)Data界面没有说明所需的密钥callback 2)Data接口使用DataItem,它需要一个参数

任何关于从这里出发的指南都将受到赞赏

1 个答案:

答案 0 :(得分:2)

我认为代表您的数据的最合理的具体类型是:

interface Callbacks {
  [k: string]: { cb: (d: any) => void };
}

interface Data {
  callbacks: Callbacks;
  [k: string]: Callbacks | Data;
}

与您所做的主要区别在于:

  • Callbacks并未在其子属性的cb函数/方法中强烈键入参数。它使用any允许任何内容。
  • Data具有类型为callbacks的必需Callbacks属性,而其他属性可以为Callbacks | Data。我知道您可能希望其他属性仅是Data,但是不幸的是,当您使用string index signature时,需要确保所有字符串属性都与之匹配,包括特定的{{1 }}属性。有很多方法可以表示更严格的约束,但是它们往往是通用类型,而不是具体类型。

无论如何,此定义将接受您的数据:

"callbacks"

但正如我所说,它还会接受您可能要禁止的一些数据:

const d: Data = {
  callbacks: {
    x: { cb: (data: number) => {} },
    y: { cb: (data: string) => {} }
  },
  foo: {
    callbacks: {
      z: { cb: (data: boolean) => {} }
    },
    bar: {
      /* .... */
    }
  }
};

请参见,const oops: Data = { callbacks: {}, foo: { throwbacks: { cb: (x: number) => {} } } // hmm }; 不是throwbacks,但是它被接受是因为callbacks的每个属性都可以采用Data的值。对于您来说,这可能并不重要。我倾向于暂时保留它,因为禁止它意味着将Callbacks设为您必须在各处指定的通用类型。

这里的另一个缺点是类型Data有很多索引签名和一个Data,当您尝试使用它时,它们会忘记对象文字的特定推断类型:< / p>

any

如果您想保留对象文字的知识,但要求它符合d.callbacks.x.cb(1); // okay d.foo; // okay d.callbacks.x.cb("1"); // oops, no error? d.flop; // oops, no error? d.foo.callbacks.z.cb(true); // oops, error? ,我在这里的建议是使用一个通用的辅助函数,该函数接受与Data匹配的任何内容并返回它的输入而不扩大它:

Data

并像这样使用它:

const dataHelper = <D extends Data>(d: D) => d;

我们可以在这里停下来,但是如果您真的想超越具体的const d2 = dataHelper({ callbacks: { x: { cb: (data: number) => {} }, y: { cb: (data: string) => {} } }, foo: { callbacks: { z: { cb: (data: boolean) => {} } }, bar: { /* .... */ } } }); d2.callbacks.x.cb(1); // okay d2.foo; // okay d2.callbacks.x.cb("1"); // error as desired d2.flop; // error as desired d2.foo.callbacks.z.cb(true); // okay as desired 类型,并且您不介意复杂性,我们可以使辅助函数强制其参数的类型严格匹配“具有类型为Data的{​​{1}}属性和所有其他属性均为类型callbacks“:

Callbacks

Datamappedconditional类型,表示仅type DataConstraint<T extends Data> = { [K in keyof T]: K extends "callbacks" ? Callbacks : T[K] extends Data ? DataConstraint<T[K]> : Data }; const dataHelper2 = <D extends Data & DataConstraint<D>>(d: D) => d; 属性应为DataConstraint类型的约束。让我们看看它是如何工作的:

"callbacks"

嘿,它给出了一个我错过的错误... Callbacks下的const d3 = dataHelper2({ callbacks: { x: { cb: (data: number) => {} }, y: { cb: (data: string) => {} } }, foo: { callbacks: { z: { cb: (data: boolean) => {} } }, bar: { // error! missing callbacks ? /* ... */ } } }); 属性缺少其必需的bar。而且我们也禁止以前的foo值错误:

callbacks

好的,希望能有所帮助;祝你好运!

Link to code