我想使用泛型使开发人员定义的对象类型安全,以避免属性中的拼写错误。这些接口可以通过属性相互交叉引用,有时作为特定类型的一维数组。
null
标记嵌套的最后一个元素,但这是我任意选择的。
该结构可以根据开发人员的需要嵌套多深,并且应该只允许更高级别属性类型的属性 - 但并非所有属性都这样做,有些是数字、字符串等。
这是基本界面结构并附有示例(
expander
):
interface Base {
id: string;
}
interface Foo extends Base {
name: string;
subFoo: Foo;
bars: Bar[];
}
interface Bar extends Base {
description: string;
foo: Foo;
}
const expander: Expander<Bar> = {
foo: null, // correct
foo: { bars: { foo: null } }, // correct
foo: { baz: null }, // should error b/c baz is not a property of Foo
baz: null, // should error b/c baz is not a property of Bar
};
到目前为止我尝试过的:
type BaseOrArray = Base | Base[];
type Expander<B extends BaseOrArray> = {
[P in keyof B]: B[P] extends BaseOrArray ? Expander<B[P]> | null : never;
}[keyof B];
但这给了我一个错误:
属性 'bars' 的类型在映射类型 '{ [P in keyof Foo]: Foo[P] extends BaseOrArray ? 中循环引用自身扩展器
|空:从不; }'.
这是一种可能的方法:
type Expander<T extends BaseOrArray> =
T extends readonly Base[] ? Expander<T[number]> : {
[K in keyof T]?: null | (T[K] extends BaseOrArray ? Expander<T[K]> : never)
};
这是一个递归条件类型。首先,您似乎希望
Expander<X[]>
等于 Expander<X>
。因此,初始条件类型只是检查 T
是否是数组,如果是,则返回 Expander<T[number]>
作为数组元素类型(我们使用 T
索引到
number
来获取数组元素类型) 。如果它不是一个数组,那么它就是一个 Base
兼容的对象。在这里,我们生成一个 映射类型,以便 K
的 T
键处的每个属性都成为可选属性(使用 ?
映射修饰符),其类型本质上是 Expander<T[K]> | null
。有一个问题是 T[K]
可能不是 BaseOrArray
,所以我们检查一下。如果是 1,那么我们得到 Expander<T[K]> | null
。否则我们只会得到 null
。
这将为
Bar
生成以下类型:
let expander: Expander<Bar>;
/* let expander: {
description?: null | undefined;
foo?: {
name?: null | undefined;
subFoo?: Expander<Foo> | null | undefined;
bars?: Expander<Bar[]> | null | undefined;
id?: null | undefined;
} | null | undefined;
id?: null | undefined;
} */
您会得到您正在寻找的行为:
expander = { foo: null }; // okay
expander = { foo: { bars: { foo: null } } }; // okay
expander = { foo: { baz: null } }; // error
expander = { baz: null } // error
看起来不错。
请注意,我想说问题中的
Expander
版本无法工作的主要原因是您将其设为索引访问类型{⋯}[keyof B]
,您可能从某处进行模式匹配?该格式看起来像在 microsoft/TypeScript#47109 中创造的“分布式对象类型”。但你真的不想这样做,因为它会生成所有属性的union,而不是对象类型。当您将其应用于像 Foo
或 Bar
这样的递归类型时,您最终会遇到循环错误,因为它需要完全下降到类型中才能产生结果。可以推迟对象类型的评估(因为一旦知道属性键,它就可以等待),但会急切地评估索引访问类型。Playground 代码链接