这是Is there an alternative to type union which includes optional properties present only in some alternatives?的递归版本.那里的解决方案给出了

type AllKeys<T> = T extends unknown ? keyof T : never;

type Exactify<T, K extends PropertyKey = AllKeys<T>> =
    T extends unknown ? T & Partial<Record<Exclude<K, keyof T>, never>> : never;

type Test = Exactify<X | Y>;

type Optionalize<T> = Omit<Exactify<T>, never>;

type X = { id: number, x: number };
type Y = { id: string, y: number };

type Z = Optionalize<X | Y>;
/* type Z = {
    id: string | number;
    x?: number | undefined;
    y?: number | undefined;
} */

然而,现在让我们假设像本例中的id这样的共享属性本身可以是对象类型.然后我们就会得到

type X1 = { shared: { x1: number }, x: number };
type Y1 = { shared: { y1: string }, y: number };

type Z1 = Optionalize<X1 | Y1>;
/* type Z1 = {
    shared: { x1: number } | { y1: string };
    x?: number | undefined;
    y?: number | undefined;
} */

如果that有任何共享对象类型属性,我更喜欢shared: { x1?: number, y1?: number }等等.

仍然可以安全地假设这些类型的对象没有任何额外的属性(包括一种类型的属性在另一种类型中缺失).

一种只适用于某一深度且易于调整的解决方案是可以接受的,但当然,如果它在任何深度都有效,那就更好了.

推荐答案

所以最初的Omit<Exactify<T>, never>个有正确的属性名称,但它没有递归到这些属性,并适用于每一个. 我们可以解决这个问题,取Omit<Exactify<T>, never>,给它一个简单的名字,比如U,然后计算{[K in keyof U]: Optionalize<U[K]>}. 我们还需要确定基本情况,所以我们不会永远递归.只有递归到对象才有意义(在可选属性的情况下,可能是undefined个对象),所以如果T可以赋值给object | undefined,我们递归,否则T是一个原始类型,我们只返回T本身. 就像这样:

type Optionalize<T> =
    [T] extends [object | undefined] ? (
        Omit<Exactify<T>, never> extends infer U ? (
            { [K in keyof U]: Optionalize<U[K]> }
        ) : never
    ) : T;

请注意,我写了[T] extends [object | undefined]而不是更明显的T extends object | undefined,以避免它是distributive conditional type并将T拆分为unions 成员.我们真的不想分裂那个联盟,因为整个问题是我们正在合并联盟.

还要注意,我使用conditional type inference with inferOmit<Exactify<T>, never>复制到类型参数U中.有其他方法可以做到这一点,但这种方法效果足够好.


好的,让我们测试一下:

type X1 = { shared: { x1: number }, x: number };
type Y1 = { shared: { y1: string }, y: number };

type Z1 = Optionalize<X1 | Y1>;
/* type Z1 = {
    shared: {
        x1?: number;
        y1?: string;
    };
    x?: number;
    y?: number;
} */

看上go 不错.请注意,像这样的递归条件类型往往具有奇怪的边缘情况,因此您应该针对一组有代表性的输入进行彻底测试,并准备好调整或重构定义以处理任何真正不可接受的事情.

Playground link to code

Typescript相关问答推荐

如何将绑定到实例的类方法复制到类型脚本中的普通对象?

为什么ESLint抱怨通用对象类型?

如何在TypeScript对象迭代中根据键推断值类型?

接口的额外属性作为同一接口的方法的参数类型'

扩展函数签名中的参数类型,而不使用泛型

TypeScrip 5.3和5.4-对`Readonly<;[...number[],字符串]>;`的测试版处理:有什么变化?

Angular NgModel不更新Typescript

为什么我的条件类型不能像我预期的那样工作?

Select 下拉菜单的占位符不起作用

APP_INITIALIZER在继续到其他Provider 之前未解析promise

如何将对象数组中的键映射到另一种对象类型的键?

Typescript 中相同类型的不同结果

如何使这种一对一关系在旧表上起作用?

打字错误TS2305:模块常量没有导出的成员

如何创建内部和外部组件都作为props 传入的包装器组件?

必需的输入()参数仍然发出&没有初始值设定项&q;错误

类型判断数组或对象的isEmpty

是否可以将类型参数约束为不具有属性?

如何通过nx找到所有受影响的项目?

如何确定单元格中的块对 mat-h​​eader-cell 的粘附(粘性)?