递归接口定义结合泛型

Recursive interface definition combined with generics

我只是 运行 我有一些讨厌的嵌套数据,我想用打字稿接口来描述这些数据。

先来看一个数据样本

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

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

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

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

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

Playground

现在我看到 2 个问题:

1) Data 接口没有说明所需的密钥 callback 2) Data 接口使用 DataItem 需要参数

任何关于从这里去哪里的指导将不胜感激

我认为表示数据的最合理的具体类型如下:

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

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

此处与您所做的主要区别:

  • Callbacks 未将参数强类型化到其子属性的 cb function/method。它使用 any 来允许任何事情。
  • Data 有一个必需的 callbacks 属性 类型 Callbacks,而其他属性可以是 Callbacks | Data。我知道您可能希望其他属性只是 Data,但不幸的是,当您使用 string index signature 时,您需要确保所有 string-keyed 属性都匹配它,包括特定的 "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 值。这对您来说可能不是什么大不了的事。我倾向于暂时保留它 as-is,因为禁止它意味着使 Data 成为您必须在任何地方指定的通用类型。

这里的另一个缺点是类型 Data 有一堆索引签名和一个 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和 returns 它的输入而不加宽它:

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 类型并且你不介意复杂性,我们可以让助手强制其参数类型严格匹配 "has a callbacks property of type Callbacks and all other properties are of type Data":

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 是一个 mapped and conditional 类型,表示只有 "callbacks" 属性 应该是 Callbacks 类型的约束。让我们看看它是如何工作的:

const d3 = dataHelper2({
  callbacks: {
    x: { cb: (data: number) => {} },
    y: { cb: (data: string) => {} }
  },
  foo: {
    callbacks: {
      z: { cb: (data: boolean) => {} }
    },
    bar: { // error! missing callbacks 
      /* ... */
    }
  }
});

嘿,它给出了一个我错过的错误... foo 下的 bar 属性 缺少所需的 callbacks。并且我们还禁止之前的错误 oops 值:

const oops2 = dataHelper2({
  callbacks: {},
  foo: { throwbacks: { cb: (x: number) => {} } } // error! not Data
});

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

Link to code