展平嵌套值对象的通用类型

Generic type of flatten nested value objects

我正在尝试为 domain/aggregate 和实体定义通用类型,这将使​​我能够构建包含多个域的聚合。总而言之,这将导致嵌套值对象 类.

为了简化 API 响应,我想提供一个 toObject() 函数,它会 return 一个带有字符串(或者更好;ValueType)的对象。

在下面的示例中,我希望 order.toObject() 到 return 最后指定的类型和对象。

我还希望能够为 BaseDomain.toObject()

定义一个 recursive/deep 通用类型

有什么想法吗?我欢迎反馈和想法

Playground

//// What I would like to solve /////

// Define a generic type that flatten a object by removing all class specific and only keeping the properties
type FlattenProperties<T> = any;

// My poor attempt
/*
type FlattenProperties<T> = T extends IBaseEntity<infer U> ? U
    : T extends IBaseList<infer U>
        ? T['items'] extends IBaseDomain<infer Z>[] ? [...T['items']]: FlattenRecursiveEntityProperties<Z>  //Not sure of how to recurse iterate a array 
        : U[]
    : T extends object ? {[K in keyof T]: FlattenRecursiveEntityProperties<T[K]>}
    : T;

*/

//// Helpers //////

interface IValue<ValueType> {
    value: ValueType
}

interface IBaseEntity<ValueType> {
    value: ValueType
}

abstract class BaseEntity<ValueType> implements IBaseEntity<ValueType> {
    readonly properties: IValue<ValueType>;
    constructor(value?: ValueType) {
        this.properties = { value: value ?? {} as ValueType };
    }
    get value(): ValueType {
        return this.properties.value;
    }

}

class ObjectId extends BaseEntity<string> {
    constructor(id?: string) {
        super()
        this.properties.value = id ?? 'GeneratedID';
    }
}

interface IBaseList<ItemType> {
    items: ItemType[]
}
abstract class BaseList<ItemType> {
    readonly items: ItemType[];
    constructor(items?: ItemType[]) {
        this.items = items ?? [];
    }
    getItems() {
        return this.items;
    }
    add(item: ItemType) {
        this.items.push(item);
    }
}

interface IBaseDomain<Interface> {
    readonly properties: Interface;
    readonly _id: ObjectId;
    toObject(): FlattenProperties<Interface>
}

abstract class BaseDomain<Interface> implements IBaseDomain<Interface> {
    readonly properties: Interface;
    readonly _id: ObjectId;
    constructor(object?: Interface, id?: ObjectId) {
        this._id = id ?? new ObjectId('#a-generated-value#');
        this.properties = object ?? {} as Interface;
    }
    get id() {
        return this._id
    }
    toObject(): FlattenProperties<Interface> {
        return {} as FlattenProperties<Interface>;
    }

}



///// Definition of domains ///////

interface IProduct {
    name: string;
    description: string
}

class Product extends BaseDomain<IProduct> {
    constructor(name: string, description: string) {
        super({ name: name, description: description })
    }
}

interface IOrderLine {
    product: Product;
    qty: number;
}

class OrderLine extends BaseDomain<IOrderLine> {
    constructor(product: Product, qty: number) {
        super({ product: product, qty: qty })
    }
}

class OrderLines extends BaseList<OrderLine> { }

interface IOrder {
    orderComment: string;
    orderLines: OrderLines
}

class Order extends BaseDomain<IOrder> {

}

///// Example run /////

const toyTruck = new Product('Toy truck', 'Yellow plastic toy truck');
const toyDuck = new Product('Duck', 'Made famous by Duck Sauce');

export const order = new Order({
    orderComment: 'Some new shiny toys',
    orderLines: new OrderLines([new OrderLine(toyTruck, 1), new OrderLine(toyDuck, 100)])
});

const htmlResponse: Expected = order.toObject(); // Type = Expected
htmlResponse.orderLines[0].product.name; // Type = string

type Expected = {
    orderComment: string,
    orderLines: {
        product: {
            name: string,
            description: string
        },
        qty: number
    }[]
}

type Expected2 = {
    orderComment: string,
    orderLines: {
        product: IProduct,
        qty: number
    }[]
}

declare var x: Expected;
declare var x: Expected2;
//declare var x: FlattenProperties<IOrder>; //TODO: Make valid

看起来 FlattenProperties<T> 应该是递归的 recursive conditional type, where the base cases are if T is a primitive type or if T extends IBaseEntity<U> for some U. If T extends IBaseDomain<U> or IBaseList<U> for some U then you can recurse into U. Or if T is an object type, you can map its properties。像这样:

type FlattenProperties<T> =
    T extends IBaseEntity<infer U> ? U :
    T extends IBaseDomain<infer U> ? FlattenProperties<U> :
    T extends IBaseList<infer U> ? FlattenProperties<U>[] :
    T extends object ? { [K in keyof T]: FlattenProperties<T[K]> } : T;

您在评论中提到您可能希望原始节点或 IBaseEntity 叶节点为 string 而不是其他类型,因此在这种情况下它看起来像:

type FlattenProperties<T> =
    T extends IBaseEntity<infer U> ? string :
    T extends IBaseDomain<infer U> ? FlattenProperties<U> :
    T extends IBaseList<infer U> ? FlattenProperties<U>[] :
    T extends object ? { [K in keyof T]: FlattenProperties<T[K]> } : string;

但现在我将使用第一个定义,因为它更笼统。让我们看看它是否有效:

type FPOrder = FlattenProperties<Order>
/* type FPOrder = {
    orderComment: string;
    orderLines: {
        product: {
            name: string;
            description: string;
        };
        qty: number;
    }[];
} */

这就是你想要的类型,所以看起来不错!


为了清楚地知道发生了什么,让我们“手动”部分评估FlattenProperties<Order>

FlattenProperties<Order>

变成

Order extends IBaseEntity<infer U> ? U :
Order extends IBaseDomain<infer U> ? FlattenProperties<U> :
Order extends IBaseList<infer U> ? FlattenProperties<U>[] :
Order extends object ? { [K in keyof Order]: FlattenProperties<Order[K]> } : T;

因为 Order 不是 IBaseEntity 我们转到第二行。 Order 是一个 IBaseDomain<IOrder>,因此 U 被推断为 IOrder 并且类型变为

FlattenProperties<IOrder>

现在我们有

IOrder extends IBaseEntity<infer U> ? U :
IOrder extends IBaseDomain<infer U> ? FlattenProperties<U> :
IOrder extends IBaseList<infer U> ? FlattenProperties<U>[] :
IOrder extends object ? { [K in keyof IOrder]: FlattenProperties<IOrder[K]> } : T;

IOrder 既不是 IBaseEntity,也不是 IBaseDomain,也不是 IBaseList。这是一个 object,所以我们现在有

{ [K in keyof IOrder]: FlattenProperties<IOrder[K]> }

变成

{
    orderComment: FlattenProperties<string>;
    orderLines: FlattenProperties<OrderLines>
}

FlattenProperties<string> 只是 string 因为 string 是一个不扩展任何 IBase* 类型的原语。而 FlattenProperties<OrderLines> 变成 FlattenProperties<OrderLine>[] 因为 OrderLines extends IBaseList<OrderLine>。所以现在我们有

{
    orderComment: string;
    orderLines: FlattenProperties<OrderLine>[]
}

我们可以继续前进,但希望您能看到这最终将如何评估为所需的类型。

Playground link to code