递归接口定义结合泛型
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 键(如 foo
和 bar
)。另请注意,函数的参数可以是任何东西!
我尝试为此数据结构创建接口的结果是:
interface DataItem<T> {
cb: (data: T) => void;
}
interface Data {
[key: string]: DataItem | Data;
}
现在我看到 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
});
好的,希望对你有帮助;祝你好运!
我只是 运行 我有一些讨厌的嵌套数据,我想用打字稿接口来描述这些数据。
先来看一个数据样本
const d = {
callbacks: {
x: { cb: (data: number) => {} },
y: { cb: (data: string) => {} }
},
foo: {
callbacks: {
z: { cb: (data: boolean) => {} }
},
bar: { /* .... */ }
},
baz: { /* .... */ }
}
注意:每个级别都有一个 callback
(必需)键和一些 运行dom 键(如 foo
和 bar
)。另请注意,函数的参数可以是任何东西!
我尝试为此数据结构创建接口的结果是:
interface DataItem<T> {
cb: (data: T) => void;
}
interface Data {
[key: string]: DataItem | Data;
}
现在我看到 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
});
好的,希望对你有帮助;祝你好运!