从接口创建类型安全的嵌套结构

问题描述 投票:0回答:1

我想使用泛型使开发人员定义的对象类型安全,以避免属性中的拼写错误。这些接口可以通过属性相互交叉引用,有时作为特定类型的一维数组。

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 ? 中循环引用自身扩展器 |空:从不; }'.

typescript
1个回答
0
投票

这是一种可能的方法:

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 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.