我想通过Generics使开发人员定义的对象类型安全,以避免属性中的拼写错误.接口可以通过属性相互交叉引用,有时作为特定类型的一维array.null标记嵌套的最后一个元素,但这是我任意 Select 的.

该 struct 可以按照开发人员想要的深度进行嵌套,并且应该只允许高级属性类型的类型的属性-但并非所有属性都这样做,有些是数字、字符串等.

这是基本的接口 struct ,其中有一个示例(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
};

到目前为止,我try 的是:

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]扩展了Base OrArray?扩展器<Foo[P]>|空:从不;}'.

推荐答案

这是一种可能的方法:

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)
  };

这是一个recursive conditional type.首先,您似乎希望Expander<X[]>相当于Expander<X>.因此,初始条件类型只是判断T是否是数组,如果是,则返回数组元素类型Expander<T[number]>(我们用index into Tnumber来获取数组元素类型). 如果它不是数组,那么它就是一个与Base兼容的对象.在这里,我们生成mapped type,以便T中的键K处的每个属性都成为可选属性(使用? mapping modifier),其类型本质上是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创造的distributive object type.但您真的不想这样做,因为它生成所有属性的union,而不是对象类型.当您将其应用于FooBar等迭代类型时,您最终会出现循环错误,因为它需要完全下降到该类型中才能产生结果.对象类型的判断可以推迟(因为一旦知道属性键,它就可以等待),但索引访问类型会被热切地判断.

Playground link to code

Typescript相关问答推荐

如何使用TypScript和react-hook-form在Next.js 14中创建通用表单组件?

调用另一个函数的函数的Typescript类型,参数相同

如何使打包和拆包功能通用?

我用相同的Redux—Toolkit查询同时呈现两个不同的组件,但使用不同的参数

rxjs forkJoin在错误后不会停止后续请求

具有泛型类方法的类方法修饰符

隐式键入脚本键映射使用

如何在另一个参数类型中获取一个参数字段的类型?

获取一个TypeScrip对象,并将每个属性转换为独立对象的联合

如何在react组件中使用doctype和html标签?

限制返回联合的TS函数的返回类型

如何将父属性类型判断传播到子属性

如何将通用接口的键正确设置为接口的键

使用RXJS获取数据的3种不同REST API

什么';是并类型A|B的点,其中B是A的子类?

覆盖高阶组件中的 prop 时的泛型推理

可选通用映射器函数出错

元素隐式具有any类型,因为string类型的表达式不能用于索引类型{Categories: Element;管理员:元素; }'

使用嵌套属性和动态执行时,Typescript 给出交集而不是并集

const 使用条件类型时,通用类型参数不会推断为 const