首页 > 解决方案 > 具有映射和条件类型的递归类型定义

问题描述

我正在尝试想出一种方法来使用 TypeORM 获得更好的类型安全性。以下是一些示例 TypeORM 实体定义。

import { BaseEntity, Entity, Column, ManyToMany, JoinTable, ManyToOne, OneToMany } from 'typeorm';

@Entity()
class Product extends BaseEntity {
  @Column({ type: 'text' })
  public name: string;

  @Column({ type: 'text' })
  public description: string;

  @ManyToMany(_ => Category, category => category.products)
  @JoinTable()
  public categories: Category[];
}

@Entity()
class Category extends BaseEntity {
  @Column({ type: 'text' })
  public name: string;

  @ManyToMany(_ => Product, product => product.categories)
  public products: Product[];

  @ManyToOne(_ => Supplier, supplier => supplier.categories, { nullable: false })
  public supplier: Supplier;
}

@Entity()
class Supplier extends BaseEntity {
  @Column('text')
  public name: string;

  @Column({ type: 'boolean', default: true })
  public isActive: boolean;

  @OneToMany(_ => Category, category => category.supplier)
  public categories: Category[];
}

我正在尝试定义一种类型,该类型仅对作为实体本身的实体的属性有效。最好用一个例子来解释:

type Relations<T extends BaseEntity> = {
  // An object whose:
  // - Keys are some (or all) of the keys in type T, whose type is something which extends BaseEntity.
  // - Values are another Relations object for that key.
}

// Some examples

// Type error: "color" is not a property of Product.
const a: Relations<Product> = {
  color: {}
}

// Type error: "name" property of Product is not something that extends "BaseEntity".
const a: Relations<Product> = {
  name: {}
}

// OK
const a: Relations<Product> = {
  categories: {}
}

// Type error: number is not assignable to Relations<Category>
const a: Relations<Product> = {
  categories: 42
}

// Type error: "description" is not a property of Category.
const a: Relations<Product> = {
  categories: {
    description: {}
  }
}

// Type error: "name" property of Category is not something that extends "BaseEntity".
const a: Relations<Product> = {
  categories: {
    name: {}
  }
}

// OK
const a: Relations<Product> = {
  categories: {
    supplier: {}
  }
}

// Type error: Date is not assignable to Relations<Supplier>
const a: Relations<Product> = {
  categories: {
    supplier: new Date()
  }
}

// etc.

到目前为止,我想出了以下内容,但它不起作用,甚至可能还没有接近正确的答案:

type Flatten<T> = T extends Array<infer I> ? I : T;

type ExcludeNonEntity<T> = T extends BaseEntity | Array<BaseEntity> ? Flatten<T> : never;

type Relations<T extends BaseEntity> = {
  [P in keyof T as ExcludeNonEntity<P>]: Relations<T[P]>;
};

标签: typescripttypeormmapped-typesconditional-typeskeyof

解决方案


我的建议是这样的:

type DrillDownToEntity<T> = T extends BaseEntity ?
    T : T extends ReadonlyArray<infer U> ? DrillDownToEntity<U> : never;

type Relations<T extends BaseEntity> =
    { [K in keyof T]?: Relations<DrillDownToEntity<T[K]>> }

TheDrillDownToEntity<T>类似于您的Flatten<T>类型与 混合ExcludeNonEntity<T>,除了它以递归方式运行。它为任意数量的嵌套提取所有数组元素类型,只保留那些可分配给BaseEntity. 观察:

type DrillTest = DrillDownToEntity<Category | string | Product[] | Supplier[][][][][]>
// type DrillTest = Category | Product | Supplier

我不知道您是否会拥有数组数组;如果你不是,你可以使它成为非递归的。但重要的是,任何最终不能分配给的类型都将BaseEntity被丢弃。

ThenRelations<T>是具有所有可选属性的类型,其键来自T,其值Relations<DrillDownToEntity<>>来自 的属性T。一般来说,大多数属性都属于 类型never,因为大多数属性本身不能分配给BaseEntity。观察:

type RelationsProduct = Relations<Product>;
/* type RelationsProduct = {
    name?: undefined;
    description?: undefined;
    categories?: Relations<Category> | undefined;
    hasId?: undefined;
    save?: undefined;
    remove?: undefined;
    softRemove?: undefined;
    recover?: undefined;
    reload?: undefined;
} */

请注意,类型的可选属性和类型never之一是相同undefined,至少没有启用编译器标志。这具有阻止您分配这些类型的任何属性的效果,除非它们是. 我发现这可能比仅仅省略这些属性要好;根据结构类型,类型的值可能有也可能没有-valued属性,而其中一种形式肯定根本没有定义的属性。--exactOptionalPropertyTypesundefined{categories?: Relations<Category>}stringname{categories?: Relations<Category>, name?: never}name

您可以使用Relations.


以下代码:

type Relations<T extends BaseEntity> = {
    [P in keyof T as ExcludeNonEntity<P>]: Relations<T[P]>;
};

由于多种原因不起作用,其中最直接的原因是您正在使用键重新映射语法来可能抑制不可BaseEntity分配的属性,但是您正在编写ExcludeNonEntity<P>whereP类型。并且不会有任何键BaseEntity因此很可能最终会排除所有键,即使您可以使其正常工作。如果要禁止键,则需要检查T[P]and not P,然后P基于此省略或包含。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。

Playground 代码链接


推荐阅读