具有映射和条件类型的递归类型定义

Recursive type definition with mapped and conditional types

我正在尝试想出一种方法来使用 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]>;
};

我的建议是这样的:

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]>> }

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

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

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

那么Relations<T>是一个所有可选属性的类型,其键来自T,其值是T的属性的Relations<DrillDownToEntity<>>。一般来说,大多数属性都是 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 类型之一是相同的,至少没有 the --exactOptionalPropertyTypes compiler flag enabled. This has the effect of preventing you from assigning any property of these types at all unless they are undefined. I find this is probably better than merely omitting those properties; a value of type {categories?: Relations<Category>} might or might not have a string-valued name property, according to structural typing,而 {categories?: Relations<Category>, name?: never} 绝对不会有定义的 name 属性。

您可以使用 Relations.

的定义验证您的示例代码是否按预期工作

以下代码:

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

不工作有几个原因,其中最直接的是你正在使用 key remapping syntax 大概抑制非 BaseEntity 可分配的属性,但你正在写 ExcludeNonEntity<P>其中 Pkey 类型。并且没有 keys 会是 BaseEntity,所以这很可能最终排除所有键,即使你可以让它工作。如果你想禁止按键,那么你需要检查 T[P] 而不是 P,然后基于此省略或包含 P。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。

Playground link to code

的改进:

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

这样,以下内容也会导致类型错误,而 :

不会导致错误
const b: Relations<Product> = {
    name: undefined
}

此答案的灵感来自 Anders Hejlsberg's PR 中的以下部分:

When the type specified in an as clause resolves to never, no property is generated for that key. Thus, an as clause can be used as a filter:

type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
type T60 = Methods<{ foo(): number, bar: boolean }>;  // { foo(): number }